Day 25: DOM Performance & Patterns
✅ Today you will learn
- How the browser renders: JS → style → layout → paint
- Layout thrashing and batching reads/writes
- requestAnimationFrame, passive listeners, lazy-loading code
- Measuring with performance.now, DevTools Performance and Lighthouse
- Patterns: module, pub/sub (observer) and a tiny store
🏢 In the real world
On the owner’s ₹8,000 phone the product page takes 4 seconds to become usable and scrolling stutters. Google measures this (INP, LCP) and ranks slower sites lower; customers leave. Most JavaScript slowness comes from a handful of mistakes – today you learn to find them with measurements and fix them with proven patterns.
How the browser draws a frame
Any JavaScript task longer than 50 ms blocks taps and scrolling (“long task”). Keep event handlers short, move heavy work off the main path (chunks, Web Workers) and update the DOM in batches.
Layout thrashing: read, then write
<button id="slow">Slow: read/write mixed</button> <button id="fast">Fast: read all, then write all</button>
<p id="t"></p>
<div id="boxes"></div>
<script>
const boxes = document.querySelector('#boxes');
for (let i = 0; i < 400; i++) {
const d = document.createElement('div');
d.style.cssText = 'height:4px;margin:1px 0;background:#fde68a;width:50%';
boxes.append(d);
}
const items = [...boxes.children];
document.querySelector('#slow').addEventListener('click', () => {
const t0 = performance.now();
for (const el of items) {
const w = el.offsetWidth; // READ (forces layout)
el.style.width = (w % 300) + 20 + 'px'; // WRITE (invalidates layout) → next read recalculates
}
document.querySelector('#t').textContent = `Mixed: ${(performance.now() - t0).toFixed(1)} ms`;
});
document.querySelector('#fast').addEventListener('click', () => {
const t0 = performance.now();
const widths = items.map(el => el.offsetWidth); // all READS first
items.forEach((el, i) => { el.style.width = (widths[i] % 300) + 20 + 'px'; }); // then all WRITES
document.querySelector('#t').textContent = `Batched: ${(performance.now() - t0).toFixed(1)} ms`;
});
</script>| Triggers layout when read after a write | Prefer |
|---|---|
| offsetWidth/Height, clientWidth, scrollTop | Read once, cache the value |
| getBoundingClientRect() | IntersectionObserver / ResizeObserver |
| getComputedStyle() | Toggle classes instead of reading styles |
| Changing width/top in a loop | transform and opacity (CSS Day 13) |
requestAnimationFrame and passive listeners
<div id="bar" style="height:6px;background:#ca8a04;width:0;position:sticky;top:0;transition:none"></div>
<div id="page" style="height:240px;overflow:auto;border:1px solid #e5e7eb;border-radius:8px;padding:10px">
<p>Scroll this article – the reading-progress bar above updates once per frame.</p>
<div style="height:900px;background:linear-gradient(#fff,#fef3c7)"></div>
</div>
<script>
const page = document.querySelector('#page');
const bar = document.querySelector('#bar');
let ticking = false;
page.addEventListener('scroll', () => {
if (ticking) return; // at most one update per frame
ticking = true;
requestAnimationFrame(() => {
const max = page.scrollHeight - page.clientHeight;
bar.style.width = (page.scrollTop / max * 100) + '%';
ticking = false;
});
}, { passive: true }); // promise not to call preventDefault → smoother scrolling
</script>Long tasks: split work into chunks
// Processing 50,000 order rows in one go blocks the page; yield between chunks
const orders = Array.from({ length: 50000 }, (_, i) => ({ id: i, total: (i * 37) % 2500 }));
const yieldToBrowser = () => new Promise(r => setTimeout(r, 0)); // browsers also have scheduler.yield()
async function totalInChunks(rows, size = 5000) {
let sum = 0, chunks = 0;
for (let i = 0; i < rows.length; i += size) {
for (const row of rows.slice(i, i + size)) sum += row.total;
chunks++;
await yieldToBrowser(); // let clicks and paints happen between chunks
}
return { sum, chunks };
}
const t0 = performance.now();
totalInChunks(orders).then(r => console.log(`₹${r.sum.toLocaleString('en-IN')} in ${r.chunks} chunks, ${Math.round(performance.now() - t0)} ms total`));A chart library used only on the admin report page should not slow down the home page: btn.onclick = async () => { const { drawChart } = await import("./chart.js"); drawChart(); }. Vite splits it into a separate file automatically.
Measure before you optimise
| Tool | Shows |
|---|---|
| performance.now() / console.time | How long one function takes |
| DevTools → Performance → Record | Long tasks (red corners), layout, paint per frame |
| Lighthouse | LCP, INP, CLS, total blocking time + suggestions |
| DevTools → Coverage | Unused JS and CSS bytes |
| Network tab (Slow 4G throttling) | What a real Indian mobile user experiences |
Patterns that keep big apps organised
// Pub/Sub (observer): parts of the app talk without knowing each other
function createEventBus() {
const listeners = {};
return {
on(event, fn) { (listeners[event] ||= []).push(fn); return () => this.off(event, fn); },
off(event, fn) { listeners[event] = (listeners[event] || []).filter(f => f !== fn); },
emit(event, data) { (listeners[event] || []).forEach(fn => fn(data)); },
};
}
// A tiny store: one state object, updates through actions, subscribers re-render
function createStore(initial, bus) {
let state = initial;
return {
get: () => state,
update(change) { state = { ...state, ...change(state) }; bus.emit('change', state); },
};
}
const bus = createEventBus();
const store = createStore({ cart: [], total: 0 }, bus);
const stopBadge = bus.on('change', s => console.log(`🛒 badge: ${s.cart.length} item(s)`));
bus.on('change', s => console.log(`💰 summary: ₹${s.total}`));
store.update(s => ({ cart: [...s.cart, 'Kaju'], total: s.total + 900 }));
stopBadge(); // unsubscribe one listener
store.update(s => ({ cart: [...s.cart, 'Ladoo'], total: s.total + 500 }));| Pattern | Solves | Seen in |
|---|---|---|
| Module | Private details + small public API | ES modules, every library |
| Observer / pub-sub | Many parts react to one change | addEventListener, Redux subscribe, Vue reactivity |
| Store (single state) | One source of truth | Redux, Zustand, our to-do app |
| Delegation | One listener for many elements | Lists, tables, menus |
| Factory | Create configured objects | createStore, createCounter |
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How would you speed up a slow page?
Measure first (Lighthouse, Performance panel). Then reduce and defer JavaScript (code-split, lazy-load), avoid long tasks (chunk work, Web Workers), batch DOM reads/writes, use rAF and passive listeners, optimise images/fonts, cache API data and re-measure Core Web Vitals.
What is reflow (layout) and how do you avoid unnecessary ones?
Reflow is the browser recalculating element geometry. It happens after DOM/style changes and is forced synchronously when you read layout properties after writing. Avoid by batching reads before writes, changing classes instead of many inline styles, animating transform/opacity and using observers instead of polling positions.
Summary
- Frames have ~16 ms; tasks over 50 ms block interaction.
- Batch DOM reads before writes; use rAF and passive listeners.
- Chunk long work, lazy-load heavy modules, measure with DevTools.
- Pub/sub and a single store keep growing apps organised.
Day 26: security and clean code – XSS, safe rendering, secrets, CSP and writing code your team will thank you for.
