Going further — keep the main thread free, debounce work, use Web Workers, animate cheaply, and lock in gains with a budget.
Why: the browser does almost everything — running your JS, responding to clicks, painting — on one "main thread". A long task hogs it, so taps feel laggy (bad INP). The goal: avoid big chunks of work that block it. Note: this lesson is the more advanced 20% — reach for it once the basics are done.
Why: events like typing, scrolling, or resizing fire dozens of times a second. Running heavy work on every one floods the main thread. Debouncing waits until the user pauses, then runs once. Note: "debounce" = wait for a quiet moment before doing the work.
function debounce(fn, ms) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), ms)
}
}
// Run search only after the user stops typing for 300ms
input.addEventListener('input', debounce(runSearch, 300))Why: a Web Worker runs JavaScript on a separate thread, so heavy work (parsing a big file, crunching numbers) doesn’t freeze the page. You talk to it with messages. Note: use it for genuinely heavy CPU work — not everyday UI logic.
// main.js — hand the work off, keep the page responsive
const worker = new Worker('/worker.js')
worker.postMessage(bigDataset)
worker.onmessage = (e) => render(e.data)
// worker.js — runs on its own thread
self.onmessage = (e) => {
const result = heavyCompute(e.data)
self.postMessage(result)
}Why: animating size or position (width, top, margin) forces the browser to re-calculate the page layout every frame — janky. Animating transform and opacity is cheap because the browser can hand them to the GPU. Note: prefer transform: translate()/scale() over changing top/left/width.
/* ❌ Janky — re-lays out the page every frame */
.box:hover { width: 320px; }
/* ✅ Smooth — handled by the GPU */
.box { transition: transform 0.2s; }
.box:hover { transform: scale(1.1); }Why: pages get slower over time as features pile up. A performance budget sets limits (e.g. max bundle size, minimum Lighthouse score) and fails the build if you cross them — so regressions get caught before they ship. Note: this keeps your hard-won speed from quietly eroding.
// Example budget: fail the build if these limits are exceeded
[
{
"resourceType": "script",
"budget": 170
},
{
"resourceType": "total",
"budget": 500
}
]