javascript 49 lines · 8 steps

Batching DOM reads and writes to avoid layout thrash

A scheduler groups all DOM measurements before mutations in a single animation frame to prevent forced reflows.

Explained by highlit
1class DOMBatcher {
2 constructor() {
3 this.reads = [];
4 this.writes = [];
5 this.scheduled = false;
6 }
7 
8 read(task) {
9 this.reads.push(task);
10 this.schedule();
11 }
12 
13 write(task) {
14 this.writes.push(task);
15 this.schedule();
16 }
17 
18 schedule() {
19 if (this.scheduled) return;
20 this.scheduled = true;
21 requestAnimationFrame(() => this.flush());
22 }
23 
24 flush() {
25 const reads = this.reads;
26 const writes = this.writes;
27 this.reads = [];
28 this.writes = [];
29 this.scheduled = false;
30 
31 const results = reads.map((task) => task());
32 writes.forEach((task, i) => task(results[i]));
33 
34 if (this.reads.length || this.writes.length) {
35 this.schedule();
36 }
37 }
38}
39 
40export const batcher = new DOMBatcher();
41 
42export function equalizeHeights(elements) {
43 batcher.read(() => Math.max(...elements.map((el) => el.offsetHeight)));
44 batcher.write((maxHeight) => {
45 for (const el of elements) {
46 el.style.height = `${maxHeight}px`;
47 }
48 });
49}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Interleaving DOM reads and writes forces the browser to recompute layout repeatedly, so grouping them by phase eliminates that cost.
  2. 2A scheduled flag ensures many enqueued tasks collapse into a single requestAnimationFrame callback instead of one per call.
  3. 3Passing read results into write callbacks lets mutations depend on measurements taken safely before any writes happen.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Batching DOM reads and writes to avoid layout thrash — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code