Stop Layout Thrashing: Master Forced Reflows for Faster Web Pages

This article explains how browsers perform layout and reflow, distinguishes asynchronous and forced synchronous reflows, shows how render‑tree caching works, illustrates why layout thrashing hurts performance, and provides practical techniques—including batch reads/writes and React hooks—to avoid costly reflows.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Stop Layout Thrashing: Master Forced Reflows for Faster Web Pages

What Is Layout?

Layout is the browser process that calculates each element’s size and position on a page. Depending on CSS, content, or parent elements, every element has explicit or implicit size information. Chrome, Edge, and Safari call this layout, while Firefox refers to it as reflow.

Asynchronous Layout

When a page’s styles change, the browser checks whether a layout is needed and then updates the render tree. To render a frame, the browser runs JavaScript, computes styles, and finally performs layout. This is called asynchronous layout (or asynchronous reflow).

Forced Synchronous Layout

Some JavaScript calls force the browser to perform layout immediately to obtain style or coordinate information, such as Element.getBoundingClientRect() which returns a bounding box synchronously.

const element = document.getElementById("item1");
const rect = element.getBoundingClientRect();
console.log(rect);

If the element’s geometry has not been calculated yet, the browser must execute a reflow right away, a situation known as forced synchronous layout (or forced synchronous reflow).

Render Tree Caching and Incremental Updates

When a page loads, the DOM is parsed without visual styles or positions. The layout engine then computes style and geometry for each visible element and stores this information in an internal data structure called the render tree.

For a simple static HTML page:

HTML is parsed into the DOM; elements have no layout.

The browser performs an asynchronous reflow, assigning style and coordinates to each visible element.

Style and coordinates are combined into the render tree, and the style information is cached for later access.

After caching, subsequent reads of style or geometry are fast. For example, calling getBoundingClientRect() on an element after the async reflow returns the cached data.

Render Tree Cache Invalidation

Most modern web apps use client‑side JavaScript (e.g., React) to create and update visible elements. When JavaScript modifies the DOM—adding, removing, or updating nodes—the render tree may become partially or fully invalidated.

For instance, inserting a modal dialog creates elements without style or position; the browser will later perform an async reflow to compute them.

const modalRoot = document.createElement("div");
modalRoot.classList.add("modal--root");
const subDiv = document.createElement("div");
const paragraph = document.createElement("div");
// Add other DOM nodes and styles as needed...

document.body.firstChild.appendChild(modalRoot);
// DOM nodes are added!

When the async reflow runs, each node receives style and position, the render tree is updated, and the new information is cached.

The browser tries to recompute only the minimal affected subtree, making reflow incremental.

Synchronous Reflow Example

The following JavaScript forces a render‑tree invalidation and then triggers a synchronous reflow:

const element = document.getElementById("modal-container");
// 1. invalidate Layout Tree
element.classList.add("width-adjust");
// 2. force a synchronous reflow. This can be SLOW!
element.getBoundingClientRect();

Calling getBoundingClientRect() on a dirty element forces the browser to perform an immediate layout, extending the JavaScript task and potentially creating a long task.

Layout Thrashing

When multiple synchronous reflows are triggered within a single task or frame, the phenomenon is called layout thrashing.

const elements = [...document.querySelectorAll(".some-class")];
for (const element of elements) {
  element.classList.add("width-adjust"); // invalidate Layout Tree
  element.getBoundingClientRect(); // force synchronous reflow
}

Repeatedly invalidating the render tree and forcing reflows can severely degrade performance, creating long tasks and lowering frame rates.

How to Avoid Layout Thrashing

Always aim to prevent layout thrashing. Common strategies include batching reads and writes.

Reading position or style information on a fully cached, styled, and positioned render tree is fast.

Writing position information marks nodes as dirty but does not immediately force a reflow.

By separating reads from writes, the expensive reflow cost occurs only once during the asynchronous reflow phase.

const elements = [...document.querySelectorAll(".some-class")];
// Do all reads
const rects = elements.map(element => element.getBoundingClientRect());
// Do all writes
elements.forEach(element => element.classList.add("width-adjust"));
// Done! Asynchronous reflow will compute positions later.

Browser APIs That Trigger Reflow

When developing web applications, be aware of APIs that can force reflow (e.g., offsetWidth, scrollTop, getComputedStyle). Use them judiciously and profile performance to avoid accidental synchronous reflows.

React Development

Using React does not eliminate layout thrashing. The declarative nature of React can hide DOM mutations that invalidate layout, especially when measuring DOM nodes inside useEffect.

function MyComponent() {
  const elementRef = React.useRef();
  // Be careful with Layout APIs in `useEffect`!
  React.useEffect(() => {
    const rect = elementRef.getBoundingClientRect();
    // do something with `rect`
  }, []);
  return <div ref={elementRef}>/* more DOM nodes... */</div>;
}

If you need to read layout after React has flushed updates, use useLayoutEffect instead.

function MyComponent() {
  const elementRef = React.useRef();
  // Use the `useLayoutEffect` hook instead if you are forcing reflow.
  React.useLayoutEffect(() => {
    const rect = elementRef.getBoundingClientRect();
    // Use the values read from `rect`
    // Writing to the DOM here will likely cause more reflow!
  }, []);
  return <div ref={elementRef}>/* more DOM nodes... */</div>;
}

Conclusion

We covered the complex lifecycle of browser reflows, including asynchronous reflow, forced synchronous reflow, and layout thrashing. Web developers must understand when and how the browser’s layout engine runs to leverage its incremental design, avoid performance bottlenecks, and deliver smooth frame rates and optimal user experiences.

layoutreflow
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.