Building an Interactive DOM Benchmark, Preliminary Results
In this livecoding session, and article recap, a web developer shows you how conducted an experiment to test the efficacy of vanilla JS.
Join the DZone community and get the full member experience.
Join For FreeAfter 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
.
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 at DZone with permission of Swizec Teller, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments