
Fixing Layout Thrashing in React: A Mobile-First Performance Guide
2024-08-29
When building mobile-first websites—especially for low-end devices—performance isn’t just a nice-to-have. It’s survival. One of the most overlooked culprits of sluggish UI is layout thrashing: a hidden performance killer that silently drains CPU cycles, causes jank, and wrecks battery life.
This post breaks down:
- What layout thrashing is
- Why it matters
- How to fix it using batched layout operations and
requestAnimationFrame - A clean, working React example
What Is Layout Thrashing?
Layout thrashing happens when your code interleaves DOM reads and writes, forcing the browser to recalculate layout multiple times per frame.
Bad Pattern: Interleaved Reads and Writes
for (let i = 0; i < 1000; i++) {
const height = element.offsetHeight; // READ
element.style.height = height + 10 + 'px'; // WRITE
}
- Each read forces a layout calculation.
- Each write invalidates it.
- Multiply that by 1,000 (not uncommon) and you’ve got a performance disaster.
Why It’s Worse on Low-End Devices
- Slower CPUs → longer layout recalculations
- Limited memory → more garbage collection
- Weaker GPUs → slower repaints
- Battery constraints → every wasted cycle matters
On high-end desktops, you might get away with it. On low-end phones, your app stutters, drains battery, or crashes.
The Fix: Batch Reads First, Then Writes
The solution is algorithmic: separate layout reads from writes. This reduces layout recalculations from O(n) to O(2)—one before the reads, one after the writes.
React Example: Batched Layout with requestAnimationFrame
import { useEffect, useRef } from 'react';
const BatchedBoxes = () => {
const boxRefs = useRef([]);
useEffect(() => {
requestAnimationFrame(() => {
// Step 1: Batch READ
const heights = boxRefs.current.map(ref => ref.offsetHeight);
// Step 2: Batch WRITE
boxRefs.current.forEach((ref, i) => {
ref.style.transform = `translateY(${heights[i]}px)`;
});
});
}, []);
return (
<div>
{[...Array(5)].map((_, i) => (
<div
key={i}
ref={el => boxRefs.current[i] = el}
style={{
margin: '10px',
padding: '20px',
background: '#eee',
transition: 'transform 0.3s ease'
}}
>
Box {i}
</div>
))}
</div>
);
};
What’s Happening Here
- We use
requestAnimationFrameto schedule layout work just before the next paint. - We read all box heights first.
- Then we write all transforms in one go.
This avoids layout thrashing and keeps animations smooth—even on low-end phones.
Why Use requestAnimationFrame?
requestAnimationFrame is a browser API that lets you schedule a function to run right before the next repaint. It’s the perfect place to batch layout work.
Benefits:
- Synchronizes with the browser’s rendering loop
- Avoids layout recalculations mid-frame
- Keeps animations smooth
- Reduces CPU usage and battery drain
Think of it like:
“Hey browser, I’ll do my layout work just before you paint—so I don’t mess up your flow.”
Real-World Impact
On low-end mobile devices, batching layout operations can:
- Reduce input lag
- Improve scroll smoothness
- Lower CPU usage
- Extend battery life
- Prevent crashes in large DOM trees
Layout thrashing is a classic example of how algorithmic thinking—in this case, separating operations into O(n) reads followed by O(n) writes—can dramatically improve performance. It’s not just clever code. It’s about respecting the constraints of your users’ devices.