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
- Prepend 1000 nodes
- Insert 1000 nodes in the middle
- Append 1000 nodes
- Drop all nodes
- 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.
Continue reading about Building an interactive DOM benchmark, preliminary results
Semantically similar articles hand-picked by GPT-4
- Livecoding #12: towards animating 10k+ elements with React
- Livecoding #13: rendering React components with canvas
- Livecoding 52: First impressions of Vue
- Livecoding #14: Mostly-smooth animation up to 4,000 elements with React and canvas
- You don't want to build your own list virtualization
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
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 ❤️