Runtime performance focuses on what happens after the page loads: identifying long tasks that block the main thread, avoiding layout thrashing, using requestAnimationFrame for smooth animations, and offloading heavy computation to Web Workers.
Tasks exceeding 50ms block the main thread, preventing user input processing and frame painting. The Performance panel highlights these with red indicators.
Alternating DOM reads and writes in a loop forces synchronous layout recalculations. The fix is batching all reads before all writes.
Schedules callbacks before the next paint, syncing with the display refresh rate. Automatically pauses in background tabs and batches with the rendering pipeline.
Run JavaScript in a background thread parallel to the main thread. Cannot access DOM but enable heavy computation without blocking user interactions.
The flame chart, long task indicators, and frame timeline together reveal exactly which functions cause jank and where frame budgets are exceeded.
Runtime performance issues cause janky animations, unresponsive interactions, and sluggish scrolling — problems that frustrate users even on pages that load quickly. While loading performance is about getting content on screen fast, runtime performance is about keeping the experience smooth after the page is interactive.
JavaScript runs on a single main thread that also handles layout, painting, and user input processing. When JavaScript executes a task longer than 50ms (a "long task"), the browser cannot respond to user input or paint new frames, causing visible jank. At 60fps, each frame has a budget of ~16ms. Exceeding this budget means dropped frames and stuttering.
The Performance panel is the primary tool for diagnosing runtime issues. Record a session, then analyze:
Flame Chart: Shows the call stack over time. Wide bars indicate long-running functions. Look for JavaScript execution, layout, and paint operations that exceed the 16ms frame budget.
Long Tasks: The panel highlights tasks over 50ms with red corners. These are the tasks blocking the main thread. Click on them to see the full call stack and identify the culprit.
Frames: The frames section shows frame rate over time. Green bars mean frames rendered within budget (16ms). Red/yellow bars indicate dropped frames where the user experienced jank.
Bottom-Up and Call Tree: These tabs aggregate time by function, showing which functions consumed the most time across the entire recording. Sort by "Self Time" to find the actual bottlenecks rather than parent wrapper functions.
Layout thrashing occurs when JavaScript alternates between reading layout properties and making DOM changes in a loop:
// BAD: Forces layout recalculation on every iteration
for (const el of elements) {
el.style.width = container.offsetWidth + 'px'; // read, then write
}
// GOOD: Batch reads, then batch writes
const width = container.offsetWidth; // read once
for (const el of elements) {
el.style.width = width + 'px'; // write only
}Reading layout properties (offsetWidth, getBoundingClientRect(), scrollTop) after a DOM mutation forces the browser to recalculate layout synchronously — called a "forced reflow." In a loop, this multiplies the cost by the number of iterations. The fix is always to batch reads before writes.
requestAnimationFrame (rAF) schedules a callback to run just before the next paint, syncing your updates with the browser's refresh cycle:
function animate() {
element.style.transform = `translateX(${position}px)`;
position += velocity;
if (position < target) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Unlike setInterval(fn, 16), rAF automatically pauses when the tab is not visible (saving CPU), syncs with the actual display refresh rate (which may be 90Hz or 120Hz, not just 60Hz), and batches updates with the browser's rendering pipeline.
Web Workers run JavaScript in a background thread, completely parallel to the main thread. They cannot access the DOM but can perform heavy computations (image processing, data parsing, complex algorithms) without blocking user interactions:
// main.js
const worker = new Worker('processor.js');
worker.postMessage(largeDataSet);
worker.onmessage = (e) => updateUI(e.data);
// processor.js
self.onmessage = (e) => {
const result = expensiveComputation(e.data);
self.postMessage(result);
};Communication happens via postMessage, which serializes data (structured clone). For large data transfers, use Transferable objects (ArrayBuffer, ImageBitmap) to move data without copying. SharedArrayBuffer enables shared memory between threads for maximum throughput.
The emerging scheduler.yield() API allows long tasks to voluntarily yield control back to the browser, letting it process user input and paint frames between chunks of work. This is the modern replacement for the older setTimeout(fn, 0) yielding pattern and is specifically designed to improve INP scores.
Long tasks (>50ms) block the main thread. Layout thrashing forces synchronous reflows in loops. requestAnimationFrame syncs visual updates with the display refresh rate. Web Workers move computation off the main thread entirely. Each technique targets a different aspect of runtime performance.
Fun Fact
The 60fps standard comes from cinema: 24fps was the minimum for smooth motion perception, but digital displays adopted 60Hz for reduced flicker. Modern displays run at 90Hz, 120Hz, or even 240Hz — and requestAnimationFrame automatically adapts, while setInterval(fn, 16) would run too slowly on a 120Hz display.