Website Performance Boosting — Part 3 — DOM
In part three of my “Performance Boosting” series, you will learn how to make your web projects more efficient and faster by optimizing the DOM.
Limit the number of HTML elements
A large DOM tree can slow down the performance of your website. You should limit the number of HTML nodes as much as you can. When loading the page — in a perfect world — only nodes that are visible to the user should be displayed. Content that only appears after scrolling or e.g. modals that only open after interaction should only then be integrated into the DOM. You should also remove nodes that have been used but are no longer needed.
When using Bootstrap with flexbox you automatically have a deeper DOM structure than when using grid. You can completely omit the level of the row element here and you can control the layout by addressing the parent container:
/* Using Bootstrap with flexbox */
<div class="container">
<div class="row">
<div class="gr-4">...</div>
<div class="gr-8">...</div>
</div>
<div class="row">
<div class="gr-4">...</div>
<div class="gr-8">...</div>
</div>
</div>
/* Using grid without Bootstrap */
<div class="container">
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
</div>
IntersectionObserver to the rescue
You can use the IntersectionObserver API to detect when and where the user scrolls, so you can lazy load your content. Check out some examples of lazy loading images and components in the lazy load section in part one of my Performance Boosting series.
Lower transfer size
If you use fewer DOM elements, the transfer size of the data will also decrease, which will especially please users of mobile devices.
At the same time, the traffic and thus the load on the server is reduced, which also has advantages for our environment. In addition, it is easier for the server to deliver websites faster when the load is low.
Runtime performance and memory performance
A large DOM with a lot of complicated CSS rules can really slow down your page rendering.
If you have an excessive DOM size and you use general query selectors such as document.querySelectorAll('div')
, you can store references to a huge amount of nodes, which can bring the memory of your users’ devices to its knees.
Avoid updating parent nodes
If you manipulate CSS properties of parent nodes using JavaScript, then it can result in all child elements also having to be re-rendered. Therefore, always try to address the direct element. This will cause the fewest recalculations.
Deliver static HTML
Let the server side generate your HTML for the critical rendering path in advance. If possible, the HTML should not be generated by a server language when called. Instead, the server scripting languages should be used to store and cache the already rendered HTML as a file. This saves the overhead (e.g. database connection) of assembling the page first when the user arrives on your website.
A basic structure of an HTML static website — sometimes it’s all you need for the critical rendering path — can look like this:
<html>
<head>...</head>
<body>
<div class="wrap">
<header class="header">
<img class="header--logo" src="logo.svg" alt="Logo">
<nav class="header--nav">
<ul>
<li><a href="">Home</a></li>
...
</ul>
</nav>
</header>
<section class="content">
<h1>Welcome</h1>
<p>Teaser Text</p>
</section>
<!-- insert dynamic content here -->
<footer class="footer">...</footer>
</div>
</body>
</html>
To add additional content to the DOM that does not affect the critical render path, there are several methods that follow now…
Facades
Lazy loading with facades means the delayed loading of third-party components, with static HTML or an image being loaded first, which is then only replaced with the correct component when the user interacts. This saves unnecessary loading of external data, mostly JavaScript, whose performance we often have no opportunity to optimize.
According to Google, the facade should be added to the DOM during the onload event of the website, with mouseover it can then be preconnected to the third-party resources and only with a click is the facade exchanged with the third-party content.
Good uses for facades are e.g. Google Maps, calendar widgets, or video players e.g. from YouTube or Vimeo.
Shadow DOM
So, now let’s go into the dark side of the DOM…
The shadow DOM is used to create custom HTML elements — known as web components — which are attached to the DOM via a shadow root. Some manipulations of the DOM (adding or removing HTML nodes) can lead to reflows and repaints of the layout, which can negatively affect the user experience. Manipulations within the shadow DOM do not affect the DOM and therefore do not cause repaints or reflows, which can speed up re-rendering.
In the following example, you can see how an external style sheet is added to the shadow DOM. Similarly, any other HTML elements can be created in the shadow DOM.
// Create an external stylesheet
const style = document.createElement("link");
style.setAttribute("rel", "stylesheet");
style.setAttribute("href", "style.css");
// Create the shadow root
const shadow = this.attachShadow({ mode: "open" });
// Attach the created stylesheet to the shadow DOM
shadow.appendChild(style);
Read more about the shadow DOM in the “Useful links” section below.
DocumentFragment
With the JavaScript DocumentFragment interface, DOM nodes can be created and then added to the active DOM.
Similar to the shadow DOM, changes to the DocumentFragment do not affect the rest of the DOM and do not degrade performance.
// Create a list of fruits
let fruits = ['Apple', 'Banana', 'Cherry', 'Pineapple'];
// Grab a empty <ul> element with the id #fruits existing in your HTML
let fruitsUl = document.querySelector('#fruits');
// Create a document fragment
let fragment = new DocumentFragment();
// Append content to the fragment
fruits.forEach((fruit) => {
let li = document.createElement('li');
li.innerHTML = fruit;
fragment.appendChild(li);
});
// Append the fragment to your <ul>
fruitsUl.appendChild(fragment);
Virtual DOM — Hydration
Hydration is a technology that delivers static HTML first, adding interaction to elements afterward by creating a so-called Virtual DOM. In the case of partial hydration, not all elements are hydrated, but only those that are meant to have interactive behavior. Here you benefit from server-side rendering to improve the First Input Delay (FID) of Google’s measurements.
Another option is progressive hydration, where individual components are loaded later as soon as they become visible on the screen.
Hydration is used in particular for single page applications. Frameworks like React or NextJS use hydration.
Useful links
Avoid an excessive DOM size
Lazy loading with facades
Shadow DOM
DocumentFragment
Hydration
Performance insights video
What’s next?
In the upcoming parts of my performance boosting series, I will focus on optimizing your server and the page speed of JavaScript.
Stay tuned…