INTRO
I've recently checked whether using a Box in Material UI 5, instead of plain div, has significant performance impact. I don't think it would matter much in practice, but read the post if you are interested in details... Doing this check got me thinking: how much faster making the little square divs would be if we were to drop React and do the DOM manipulation with plain JS? Let's find out!
DISCLAIMER
I'm not suggesting that you should ditch the comforts of React/MUI and write your programs with vanilla JS just because it's faster to create tens of thousands of DOM elements this way. It's best to avoid making too many elements (think of paging and virtualization), and if you must, try to memoize to avoid repeating expensive operations...
STRESS TEST APP
Just like the program from previous post, the test app for this post is also about making lots of small squares with varying colors (through randomized opacity). It was made with Vite Vanilla + TypeScript template (vite: 5.4.1, typescript: 5.5.3).
Live demo is here: https://morzel85.github.io/blog-post-vanilla-div-performance
The code is here: https://github.com/morzel85/blog-post-vanilla-div-performance
The app has an input for choosing amount of squares to create inside a container div (flexbox with wrapping), a few buttons to choose different ways of making the divs and a button for clearing all squares. Clearing is done by setting container.innerHTML to empty string and is included before making the squares so the items don't add up.
The Make: createElement (inline) + appendChild calls this function (here inline styles are used, just like in the previous app):
const makeCreateElementInline = () => {
clear();
for (let i = 0; i < amount; i++) {
const div = document.createElement('div');
div.style.backgroundColor = 'darkorange';
div.style.opacity = Math.random().toString();
div.style.margin = '1px';
div.style.width = '15px';
div.style.height = '15px';
containerDiv.appendChild(div);
}
clearButton.disabled = false;
};
The Make: createElement + appendChild calls this function:
const makeCreateElement = () => {
clear();
for (let i = 0; i < amount; i++) {
const div = document.createElement('div');
div.className = 'item';
div.style.opacity = Math.random().toString();
containerDiv.appendChild(div);
}
clearButton.disabled = false;
};
This time a CSS class is used to apply repetitive styles (all except opacity):
.item {
background-color: darkorange;
margin: 1px;
width: 15px;
height: 15px;
}
The functions above use old-school for loop to add items, while in previous post I've used Array.from({ length: amount }, (_, i) => ... ) to generate items. The Array.from feels more natural in functional component but is slower. It is still fast enough that it just doesn't matter here: around 200 ops/s for Array.from vs 1300 ops/s for classic for loop on a test that fills array of 100K items).
Both functions use createElement and appendChild from DOM API.
The Make: string + innerHTML buttons calls this function:
const makeInnerHTML = () => {
clear();
let s = '';
for (let i = 0; i < amount; i++) {
const d = `<div class="item" style="opacity: ${Math.random()}"></div>`;
s += d;
}
containerDiv.innerHTML = s;
clearButton.disabled = false;
};
If you have C#/Java background like me you may think that code above contains a classic blunder: not using string builder. Well, it turns out that JS engines are pretty good at string concatenation. Check out this great post.
The final Make: createDocumentFragment + appendChild calls this function:
const makeCreateDocumentFragment = () => {
clear();
const fragment = document.createDocumentFragment();
for (let i = 0; i < amount; i++) {
const div = document.createElement('div');
div.className = 'item';
div.style.opacity = Math.random().toString();
fragment.appendChild(div);
}
containerDiv.appendChild(fragment);
clearButton.disabled = false;
};
The idea here is to first append all items to a fragment (obtained by createDocumentFragment) that is not a part of DOM tree and only when it's done, append the fragment to DOM. It used help in older browsers, just like the innerHTML solution shown before...
I've included couple of different methods of adding elements but the results below will show only the behavior of Make: createElement (inline) + appendChild to stay close to React version and to prevent this post from getting too log.
I've clicked a bit and to be honest I haven't noticed a significant difference that would justify switching from simplest createElement + appendChild to using innerHTML or createDocumentFragment... But, the buttons are there for you to try it yourself.
DESKTOP RESULTS
Just like before, I'm running the test on 7 years old PC with Intel Core i7-7700K, 16 GB RAM and MSI GTX 1080 Ti with Chrome 128 on Ubuntu 20.04.
That's what my eyes could see (time between button press and items appearing/page becoming responsive):
- 500, 1K, and 2K: items appear instantly.
- Barely perceptible lag starts to appear around 4K items but event at 8K it could pass as instant.
- For 15K: about 0.3s.
- For 50K: around a second.
Here's the performance trace for 10K items (just mind DevTools instrumentation is not free):
Not bad, right?
OK, let's check 25K:
Half a second, sweet!
How about a 100K?
Ok, finally we got Chrome to sweat a bit, but still... 2.3s for one hundred thousand divs! Surely the browser is smart enough not to paint the squares which are far off-screen but the divs are there (check the Nodes count on the screenshot, and document.querySelectorAll('*').length gives more than 100K too).
One thing worth mentioning here is the scrolling performance: scrolling through the absurd block of 100K squares could get really slow, it could freeze the browser for a few seconds depending on the the distance. For the more "reasonable" 20K it was pretty smooth...
Quick note about Firefox: I've run the abusive 100K check in Firefox 130 and div making was even a bit faster than in Chrome plus there was no scrolling issue! The flip side: I noticed that getting back to a tab with this many elements was bit laggy.
MOBILE RESULTS
Like before, tests were done with my 3+ years old, non-flagship, Samsung Galaxy A52 on Android 14 (this time in Chrome 128 instead of 127).
This is the perceived performance I got (time between clicking a button and page with generated items being responsive):
- 500, 1K, 2K items: instant.
- 3K: barely perceptible delay.
- 5K: about 0.2s.
- 10K: about 0.4s
- 50K: about 1.2s
- 100K: about 3s.
Honestly I'm shocked how fast it is on a phone worth about 250 USD (1000 PLN) - I mean a new one, mine fell a bit to many times without a case to be worth that much ;)
There's not much difference between the phone and my (old) PC in the div-making speed. My greatest surprise is the scrolling: it didn't freeze the browser even with 100K elements! The worst I've seen was empty space shown for a while but scrollbar stayed responsive all the time (it actually performed way better than on desktop Chrome)!