DOM Manipulation, an Expensive Narrative

DOM Manipulation, an Expensive Narrative

Today, I will be sharing some knowledge of DOM manipulation. We have seen how we directly manipulate the DOM, which is an expensive task in terms of performance, especially if done repeatedly or inefficiently. This is because every modification to the DOM can trigger a series of resource-intensive processes in the browser.

Okay! Let's take a look at why direct DOM manipulation is expensive.

Reflow and Repaint

  • Reflow: When the DOM is modified (e.g. adding or changing an element’s size or position), the browser recalculates the layout of all affected elements. This process is called reflow, and it’s computationally intensive.
  • Repaint: After reflow, the browser redraws the affected pixels on the screen. This is called a repaint.

Impact on Rendering Pipeline

  • The browser rendering the pipeline involves several steps: Parsing HTML/CSS Calculating styles Layout/reflow Paint Compositing layers for rendering.

Direct DOM manipulation can force the browser to restart parts of this pipeline multiple times, slowing down the performance.

Browser Optimization and Limits

Browsers try to optimize by grouping some changes, but they can't always batch everything perfectly, especially when changes are forced synchronously. Direct DOM manipulation often forces synchronous updates, preventing effective batching. Frequent direct DOM manipulation can disrupt this optimization, leading to slow rendering, janky animations, slow scrolling, and an overall unresponsive feel.

Below is the code which is an expensive task:

const container = document.getElementById('container');

// Inefficient: Manipulating DOM directly in a loop
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i + 1}`;
  container.appendChild(div); // Triggers reflow/repaint 1000 times
}        

This code snippet illustrates the performance problem. Each appendChild call triggers a reflow and repaint. Performing this within a loop results in 1000 reflows and repaints, which is extremely inefficient. Reflows can involve recalculating the positions and sizes of many elements, potentially affecting the entire page layout. Repaints require the browser to redraw parts of the screen, which can be complex, especially with complex CSS styles.


There’s also the issue of "forced synchronous layout" (sometimes called "layout thrashing", I will talk about this more in a future article). This happens when you read a layout property (like offsetWidth or getBoundingClientRect()) immediately after making a DOM change. This forces the browser to perform a reflow immediately to calculate the value, even if it could have potentially waited and batched the change. This is a common performance trap.


Optimizing DOM Manipulation

One of the ways to minimize the cost of DOM manipulation is by using document.createDocumentFragment(). document.createDocumentFragment() is a method in the DOM API that creates a lightweight, minimal document-like object known as a DocumentFragment. It is an in-memory container that allows you to batch-create and manipulate DOM nodes without directly impacting the live DOM until it’s explicitly appended. It exists only in memory. A DocumentFragment is part of the Document object but is not part of the document’s main DOM tree. It does not trigger reflows or repaints while you manipulate its contents. Only when you append the fragment to the DOM does the browser render the changes in a single operation. When you append a DocumentFragment to a DOM element, its child nodes are moved (not copied) into the DOM, and the fragment is left empty.

Below is the code to improve performance

const container = document.getElementById('container');
const fragment = document.createDocumentFragment();

for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i + 1}`;
  fragment.appendChild(div); // Appends to the fragment - no DOM update yet
}

container.appendChild(fragment); // Only ONE reflow and repaint here!        

document.createDocumentFragment() create an empty, lightweight, in-memory container. This allows you to batch operations on DOM nodes in memory without making immediate changes to the actual page's layout. Since it’s not part of the DOM tree, there’s no need to worry about costly reflows or repaints during this phase.

for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i + 1}`;
  fragment.appendChild(div); // Appends to the fragment - no DOM update yet
}        

Inside the loop, the code creates 1000 div elements and appends each one to the fragment. At this point, the div elements are not yet part of the live DOM on the page; they are simply in the DocumentFragment. Normally, if you were to append these 1000 div elements directly to the DOM (e.g., container.appendChild(div) within the loop), it would cause 1000 reflows and repaints (one for each append). But by appending all the elements to the DocumentFragment first, you avoid triggering reflows or repaints during the loop.

container.appendChild(fragment); // Only ONE reflow and repaint here!        

This line appends the DocumentFragment to the actual DOM (container). Since the fragment contains all the 1000 div elements, they are now added to the live DOM in one single operation. By appending the fragment to the DOM in one go, you trigger just one reflow and repaint instead of 1000. The browser calculates the layout and redraws the page only once, rather than performing that expensive operation 1000 times.

Summary of Improvement:

By using the DocumentFragment, you prevent the browser from triggering reflows and repaints during the loop. Once all the elements are appended to the fragment, you append the fragment to the DOM in one go, which only triggers a single reflow and repaint. This results in a significant performance improvement, especially when dealing with large numbers of DOM manipulations.

Another thing which popped up in my head was how it is different from "document.createElement"?

The key difference lies in when the browser updates the DOM. document.createElement() creates a single element node. When you create an element and append it to the DOM using createElement, the element is appended to the DOM along with its children, and the browser immediately performs a reflow and repaint. Whereas DocumentFragment effectively disappears when it is added to the DOM and only the children are appended.


Additional Optimizations

  • Event Listeners and DOM Manipulation

DOM manipulation can also affect event listeners. Adding event listeners after modifying the DOM can be costly if done repeatedly. It’s better to delegate events (using EventDelegation), where you attach one event listener to a parent element and handle events for child elements dynamically.

const container = document.getElementById('container');

// Delegate event to the container
container.addEventListener('click', (event) => {
  if (event.target && event.target.matches('div.item')) {
    console.log(`Item clicked: ${event.target.textContent}`);
  }
});        

  • CSS Transitions and Animations

Be aware that manipulating DOM elements during animations or transitions can cause additional performance hits. Browsers have optimizations like GPU acceleration for certain CSS properties (e.g., transform and opacity), which are cheaper to animate, as opposed to properties like width or height, which trigger reflow and repaint.

  • Intersection Observer API

For performance optimization when you need to know when an element enters or exits the viewport, consider using the Intersection Observer API. It avoids the need for excessive DOM checks or event listeners (such as scroll or resize), helping you track visibility changes without degrading performance.


Conclusion

DocumentFragments are essential for optimizing DOM manipulation. By using them, you can minimize the number of reflows and repaints, leading to smoother and more responsive web applications. This is particularly crucial when dealing with large numbers of DOM changes. Additionally, techniques like event delegation, batching updates, and using CSS transitions and animations effectively will further enhance the performance of your web apps. Always consider using "DocumentFragments" when you need to add multiple elements to the DOM and take advantage of modern APIs like Intersection Observer to avoid unnecessary reflows and repaints.

By incorporating these practices into your development workflow, you can significantly improve the user experience and ensure faster, more responsive applications.

要查看或添加评论,请登录

Siddharth Hembrom的更多文章

社区洞察

其他会员也浏览了