Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Building an interactive DOM benchmark, preliminary results

    This is a Livecoding Recap – an almost-weekly post about interesting things discovered while livecoding. Usually shorter than 500 words. Often with pictures. Livecoding happens almost every Sunday at 2pm PDT on multiple channels. You should subscribe to My Youtube channel to catch me live.

    After that chatroom windowing feature I built a few weeks ago, I've been thinking a lot about how fast native DOM manipulation has gotten.

    I've been listening to the "DOM is slow" mantra my whole life. But with that feature, I discovered that the DOM is pretty fast, actually.

    Did you know it's faster and easier to throw out your DOM nodes and render from scratch? That surprised me.

    So I started working on this DOM manipulation benchmark. My goal is to give you a live benchmark that you can play with. A way to feel how fast or slow different approaches are.

    It's not complete yet. You can already test React using local state, a naive vanilla JS implementation, and a smart vanilla JS implementation. I'm adding Vue, Preact, Angular, and maybe some others.

    The test focuses on long flat lists. Those are the most common source of having thousands of nodes. Think chatrooms, news feeds, comments, data viz. All long flat lists of nodes.

    We're testing how fast it is to

    1. Prepend 1000 nodes
    2. Insert 1000 nodes in the middle
    3. Append 1000 nodes
    4. Drop all nodes
    5. Remove 1 random node

    React using local state

    React is fast. Between 15ms and 21ms on average to prepend, insert, and append 1000 nodes. Removing 30,000 nodes is slower because it has to change a lot of individual nodes.

    The benchmark benefits from nodes being stable. That means a node with the same key never changes. You can see the code on GitHub.

    We manipulate a list in this.state and measure times between componentWillUpdate and componentDidUpdate. This should exclude the time it takes to manipulate our list.

    What surprised me is the stark difference between dev-mode React and production-built React. React in dev mode is not only slow, but also gets increasingly slower the more nodes you're rendering. In production mode performance is constant.

    Neat. 👌

    Naive vanilla JS

    The naive approach is slow. Faster than you'd expect, good for small lists, but not a scalable solution. The more nodes you render, the slower it gets.

    The core of this benchmark is the render code. Drops all nodes and renders from scratch every time.

        naiveRender() {
            let start = new Date();
            // remove all existing nodes
            // from https://stackoverflow.com/questions/3955229/remove-all-child-elements-of-a-dom-node-in-javascript
            let scratchpad = this.refs.scratchpad;
    
            while (scratchpad.firstChild) {
                scratchpad.removeChild(scratchpad.firstChild);
            }
    
            // append all nodes from scratch
            this.nodes.forEach(k => {
                let node = document.createElement("div");
                node.appendChild(document.createTextNode(k));
                scratchpad.appendChild(node);
            });
    
            let end = new Date();
            this.times.push(end - start);
    
            // update meta info
            this.refs.time.innerHTML = `<code>${end - start}ms</code>`;
            this.refs.currentCount.innerHTML = this.nodes.length;
            this.refs.avgTime.innerHTML = this.averageTime;
        }
    

    You can see the full benchmark on GitHub.

    This renders 1000 nodes in about 4ms, which is faster than React. But 4000 takes 32ms. Almost three times as much as React.

    Curiously similar performance curve to dev-mode React. 🤔

    Smart vanilla JS

    A slightly smart vanilla JS approach is blazing fast. Constant performance around 2ms. Wat 🤨

    I used the new prepend() and append() DOM methods. Nothing super clever, just a basic implementation of what we're testing.

    You can see the whole benchmark on GitHub, but here's the prepend code for example.

    prepend = () => {
      let nodes = this.newNodes,
        scratchpad = this.refs.scratchpad;
      this.nodes = [...nodes, ...this.nodes];
    
      let start = new Date();
    
      nodes.map((k) => {
        let node = document.createElement("div");
        node.appendChild(document.createTextNode(k));
        return node;
      });
      scratchpad.prepend(nodes);
    
      this.updateMeta(start);
    };
    

    We're still manipulating the this.nodes array. Keeps "data" and DOM in sync, stays consistent to the other implementations.

    Then we walk through the list of new nodes, create div elements for each of them, attach some text, and finally prepend() the whole array of nodes into the DOM. This, it turns out, is fast. 🔥

    1000 nodes in 1ms to 2ms. A tenth of the time it takes React to do it. Similar results for inserting and appending:

    • appends in about 2ms
    • inserts in about 5ms

    Inserting is slowest because there's no magic method for it. You have to insertBefore each DOM node individually. The browser then has to shove the entire list around to make room.

    Even dropping all nodes is crazy fast despite dropping 1-by-1 👉 around 2ms for 40,000 nodes. Wow.

    Conclusion

    In conclusion, vanilla JavaScript is fast if you know what you're doing. Faster probably than anything else I can add to this benchmark.

    But it's not as powerful as React or a similar framework. It takes more time to build, it's hyper optimized for this example, and it wouldn't scale in a real world environment.

    Code is hard to maintain. You waste time thinking about rendering instead of what you're building, and you will cry as soon as nodes stop being stable. Just imagine figuring out by hand which nodes' contents changed and which didn't.

    That's what React is doing for you 👉 Spending a little runtime to save a lot of dev time.

    Can't wait to add Vue and Preact to the mix.

    Published on February 27th, 2018 in Front End, JavaScript, Livecoding, Technical

    Did you enjoy this article?

    Continue reading about Building an interactive DOM benchmark, preliminary results

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️