Intermediate to senior

Frontend Interview Prep

Fourteen chapters on HTML and CSS, core JavaScript, the event loop, browser rendering, React, state, performance, accessibility, security, TypeScript, testing, machine-coding components and frontend system design, with tested code.

Chapter 6 of 14Browser and React · Browser Rendering and Web Performance

Browser Rendering and Web Performance

"Why is this page slow?" is one of the most common senior frontend questions. A strong answer starts from how the browser turns bytes into pixels, names the metrics that matter, and walks a measure-first process rather than listing tricks. This chapter covers the rendering pipeline, Core Web Vitals, loading strategy, runtime performance, caching and bundle size.

1. What happens when you type a URL

  1. URL parsing and cache checks (browser, service worker, HTTP cache).
  2. DNS lookup to find the IP address.
  3. TCP connection (and TLS handshake for HTTPS). HTTP/3 uses QUIC over UDP and merges these steps.
  4. HTTP request; the server responds with HTML (possibly after redirects).
  5. Parsing: the browser builds the DOM from HTML while discovering CSS, scripts, images and fonts.
  6. Render pipeline (next section) turns DOM and CSS into pixels.
  7. JavaScript executes and may change the DOM, triggering more rendering. For client-rendered apps, the page is empty until the JS bundle downloads, runs and fetches data.

Each step suggests an optimisation: fewer redirects, CDN and caching, preconnect, compressed and smaller responses, server rendering, less blocking JavaScript.

2. The rendering pipeline

<!--fig:pipeline-->
HTMLparse DOMtree of nodes CSSOMstyles Render treevisible boxes Layoutsize and position Paintdraw layers CompositeGPU, final frame ParsingBuildCost grows to the right Changing a layout property (width, margin, top) re-runs Layout, Paint and Composite. Changing colour re-runs Paint and Composite. Changing transform or opacity re-runs only Composite. Figure 1. From HTML and CSS to pixels. Cheaper changes skip earlier stages.
  1. DOM is built from HTML. CSSOM is built from CSS.
  2. Render tree: DOM nodes that are visible, combined with computed styles.
  3. Layout (reflow): compute the size and position of every box.
  4. Paint: draw pixels into layers (text, colours, borders, shadows).
  5. Composite: combine layers on the GPU into the final frame.

Blocking behaviour:

  • CSS is render-blocking: the browser will not paint until stylesheets in the head are loaded, because it needs them to know what to draw.
  • Synchronous scripts are parser-blocking: HTML parsing stops while the script downloads and runs. Use defer (download in parallel, run after parsing, in order) or async (run as soon as downloaded, any order) or type="module" (deferred by default).
  • A script may wait for pending CSS, because it could query styles.

Which changes cost what

ChangeTriggersCost
width, height, margin, top, font-size, adding or removing nodeslayout, paint, compositehighest
color, background, box-shadow, visibilitypaint, compositemedium
transform, opacity (on their own layer)composite onlylowest

That is why animations should use transform and opacity.

Layout thrashing

Reading a layout property (offsetHeight, getBoundingClientRect, scrollTop) forces the browser to compute layout now. Interleaving reads and writes in a loop forces repeated layouts.

// bad: each iteration writes, then reads, so layout is recalculated every time
//   for (const el of items) { el.style.width = box.offsetWidth + "px"; }
// good: batch all reads, then all writes
function batchedMeasureThenWrite(items, measure, write) {
  const sizes = items.map(measure);               // reads
  items.forEach((el, i) => write(el, sizes[i]));  // writes
  return sizes;
}

let layouts = 0, dirty = false;
const el = n => ({
  read() { if (dirty) { layouts++; dirty = false; } return 100 + n; },   // a read after a write forces layout
  write() { dirty = true; },
});
const nodes = [el(1), el(2), el(3), el(4)];

for (const n of nodes) { n.write(); n.read(); }               // interleaved
const interleaved = layouts;

layouts = 0; dirty = false;
const reads = nodes.map(n => n.read());                        // all reads first
nodes.forEach(n => n.write());                                 // then all writes
assert.equal(interleaved, 4);
assert.equal(layouts, 0);                                      // no forced layout during the batch
assert.equal(reads.length, 4);

3. Core Web Vitals and other metrics

Google's Core Web Vitals are field metrics for real-user experience (verify the current thresholds on web.dev, since they are revised occasionally):

MetricMeasures"Good" threshold (at the time of writing)
LCP (Largest Contentful Paint)how fast the main content appears2.5 s or less
INP (Interaction to Next Paint)responsiveness to clicks, taps and keys across the visit200 ms or less
CLS (Cumulative Layout Shift)visual stability: unexpected movement0.1 or less

Others: TTFB (time to first byte), FCP (first contentful paint), TBT (total blocking time, a lab proxy for responsiveness), TTI, and bundle size. Assess at the 75th percentile of real users, split by device and network.

Lab versus field: Lighthouse and DevTools give reproducible lab data for debugging; the Chrome UX Report and your own real-user monitoring (RUM) show what users experience. Use both.

Improving each vital

LCP: make the hero resource discoverable and fast. Server-render or pre-render the HTML; put the hero image in the HTML (not injected by JavaScript) with fetchpriority="high"; do not lazy-load it; compress and size images; preload critical fonts and images; use a CDN; reduce render-blocking CSS and JS; cut TTFB.

INP: keep the main thread free. Break up long tasks (over 50 ms), defer non-critical work, reduce JavaScript, avoid expensive handlers and large DOMs, use startTransition or debouncing, and move heavy computation to workers.

CLS: reserve space. Set width and height (or aspect-ratio) on images, videos and embeds; reserve slots for ads and banners; avoid inserting content above existing content; use font-display and size-adjusted fallback fonts; animate with transform.

4. Loading strategy

  • Critical path: minimise the bytes and round trips needed for first render. Inline critical CSS; defer the rest.
  • Resource hints: preconnect (early connection to an origin), dns-prefetch, preload (fetch something needed soon), prefetch (fetch for a likely next navigation), modulepreload.
  • Images: modern formats (AVIF, WebP), responsive srcset, loading="lazy" for below-the-fold images, correct dimensions, a CDN with on-the-fly resizing.
  • Fonts: subset, WOFF2, font-display: swap or optional, preload the key font, self-host.
  • Compression: Brotli or gzip for text.
  • Third-party scripts (analytics, chat, ads) are a frequent culprit: load them async or after interaction, audit their cost, and sandbox where possible.

5. Rendering strategies

StrategyHTML comes fromStrengthsTrade-offs
CSR (client-side rendering)an empty shell plus JavaScriptsimple hosting, rich interactivityslow first paint, weaker SEO, heavy JS
SSR (server-side rendering)the server, per requestfast first paint, good SEO, fresh dataserver cost, TTFB, hydration cost
SSG (static generation)built ahead of timefastest, cheap, cacheablestale until rebuilt
ISR / revalidationstatic, regenerated in the backgroundfresh enough, fastmore moving parts
Streaming SSRserver, in chunksearly shell, slow parts latercomplexity
Islands / partial hydration, server componentsmostly HTML with interactive piecesless JavaScript shippedframework support, boundaries to design

Hydration attaches event handlers to server-rendered HTML. The page looks ready but may not respond until hydration finishes, which hurts INP; mitigate with less JavaScript, selective or lazy hydration and server components.

6. JavaScript and bundle size

  • Measure: use a bundle analyser; look for large libraries (date, charts, lodash) and duplicates.
  • Code-split by route and by feature with dynamic import(); lazy-load components that are below the fold or behind interaction.
  • Tree-shake with ES modules and side-effect-free packages; import functions, not whole libraries.
  • Replace heavy dependencies with lighter ones or native APIs.
  • Modern syntax targets (no unnecessary polyfills) and minification.
  • Budget: set a size budget and fail CI when exceeded.
// a tiny lazy loader: load a module once, on first use
function lazy(loader) {
  let promise;
  return () => (promise ??= loader());
}
let loads = 0;
const getChart = lazy(async () => { loads++; return { render: () => "chart" }; });
const [c1, c2] = await Promise.all([getChart(), getChart()]);
assert.equal(loads, 1);                              // concurrent callers share a single load
assert.equal(c1, c2);

7. Runtime performance

  • Long tasks: anything over 50 ms on the main thread delays input. Chunk work, yield to the browser, use workers or idle time.
  • Virtualise long lists (render only visible rows) instead of thousands of DOM nodes.
  • Avoid unnecessary re-renders in frameworks (see the React chapter), but measure first.
  • Debounce and throttle high-frequency events; use passive listeners for scroll and touch; use IntersectionObserver instead of scroll polling.
  • Memory: detach listeners, clear timers and subscriptions, avoid retaining detached DOM nodes in closures, and use Chrome DevTools heap snapshots to find leaks.
  • Animations: transform and opacity, requestAnimationFrame, and prefers-reduced-motion.
// windowing: how many rows to render for a viewport
function visibleRange(scrollTop, viewportHeight, rowHeight, total, overscan = 2) {
  const first = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const last = Math.min(total - 1, Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan);
  return [first, last];
}
assert.deepEqual(visibleRange(0, 400, 40, 10000), [0, 12]);       // about a dozen rows rendered out of ten thousand
assert.deepEqual(visibleRange(4000, 400, 40, 10000), [98, 112]);
assert.deepEqual(visibleRange(399960, 400, 40, 10000), [9997, 9999]);

8. Caching

  • HTTP caching: Cache-Control (max-age, immutable, no-store, stale-while-revalidate), ETag and conditional requests. Fingerprint build assets (app.3f9a1c.js) and cache them for a year; keep HTML short-lived.
  • CDN for static assets and edge caching.
  • Service workers and the Cache API enable offline and instant repeat loads, with strategies (cache-first, network-first, stale-while-revalidate).
  • Client data cache: libraries such as TanStack Query or SWR cache and dedupe API responses.
  • localStorage and sessionStorage are synchronous and block the main thread; avoid heavy use. IndexedDB is asynchronous.

9. A measure-first workflow

  1. Reproduce on a realistic device and network (throttle CPU and network in DevTools).
  2. Measure: Lighthouse for a first pass, the Performance panel for long tasks and layout, the Network panel for waterfalls and blocking resources, a bundle analyser for size.
  3. Find the biggest bottleneck (not the easiest fix): LCP resource, main-thread work, third parties, images, waterfalls.
  4. Change one thing, re-measure, and keep what helps.
  5. Guard it: performance budgets in CI and RUM dashboards for regressions.

10. Common mistakes

  • Optimising without measuring.
  • Lazy-loading the hero image (hurts LCP).
  • Shipping a giant bundle for a page that needs a fraction of it.
  • Images without dimensions (layout shift).
  • Unbounded third-party scripts.
  • Reading layout properties inside write loops.
  • Rendering thousands of DOM nodes with no virtualisation.
  • Equating a high Lighthouse score with good real-user experience.

11. Practice questions

  1. Walk through what the browser does from receiving HTML to showing pixels.
  2. What is render-blocking? How do async and defer differ?
  3. Explain LCP, INP and CLS and one fix for each.
  4. Why is transform cheaper to animate than top? What is layout thrashing?
  5. Compare CSR, SSR and SSG. What is hydration and what is its cost?
  6. How would you debug a page that feels slow on a mid-range phone?
  7. How would you render a list of 100,000 items?
  8. Design a caching strategy for a static site with an API.
Header Logo