ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

Web Performance Interview Questions

Short answer

Web performance interviews centre on three things: the Core Web Vitals and what actually causes each one, the critical rendering path from HTML to first paint, and how you would diagnose a slow page. The answer that scores always starts with measurement, not with a list of optimisations.

MetricWhat it measuresGoodUsual cause when it is bad
LCPWhen the main content appears≤ 2.5sSlow server, render-blocking CSS, huge hero image
INPResponsiveness to interaction≤ 200msLong tasks blocking the main thread
CLSUnexpected layout movement≤ 0.1Images without dimensions, injected banners, late fonts
TTFBServer response time≤ 800msSlow backend, no CDN, no caching
FCPWhen anything appears≤ 1.8sRender-blocking resources in the head
Core Web Vitals — the thresholds and the usual culprit for each.

What are the Core Web Vitals?

Three metrics Google uses as a ranking signal and, more usefully, as a proxy for how a page feels. Largest Contentful Paint measures when the biggest element in the viewport finishes rendering — effectively when the user sees the thing they came for. Interaction to Next Paint measures how quickly the page responds across the whole visit, replacing First Input Delay in 2024 because responsiveness matters after the first click too. Cumulative Layout Shift measures how much content moves unexpectedly.

The reason to know the thresholds is that they turn a vague question into a specific one. "Make the page faster" is unanswerable; "LCP is 4.2 seconds and we need it under 2.5" tells you to look at the server response, render-blocking resources in the head, and whatever the largest element is — usually a hero image. Quoting the number you are aiming at is a small thing that makes an answer sound like it came from doing the work.

What is the critical rendering path?

The sequence the browser follows from receiving HTML to painting pixels: parse the HTML into a DOM, parse the CSS into a CSSOM, combine them into a render tree, calculate layout, then paint. Anything that blocks a step delays first paint, and the two things that block are stylesheets and synchronous scripts in the head.

What blocks, and what does nothtml
1<!-- BLOCKS parsing and rendering -->
2<script src="/analytics.js"></script>
3<link rel="stylesheet" href="/everything.css">
4
5<!-- Does NOT block parsing -->
6<script src="/analytics.js" defer></script>   <!-- runs after parse, in order -->
7<script src="/widget.js" async></script>      <!-- runs whenever it lands -->
8
9<!-- Load non-critical CSS without blocking the first paint -->
10<link rel="preload" href="/below-fold.css" as="style"
11      onload="this.rel='stylesheet'">
12
13<!-- Tell the browser what matters early -->
14<link rel="preconnect" href="https://api.example.com">
15<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
16<!-- → the hero starts downloading before the parser reaches the <img> -->

The distinction between `defer` and `async` is asked directly and often. Both download in parallel with parsing. `defer` waits until parsing finishes and preserves the order scripts were written in, which makes it the right default for application code with dependencies. `async` executes the moment it arrives, in whatever order that happens to be, which suits genuinely independent third-party scripts and nothing else.

How do you fix a bad LCP?

Find what the LCP element actually is first — Lighthouse and the Performance panel both tell you, and it is usually a hero image, a heading, or a block of text held back by a web font. Then the fix depends on which. For an image, serve it in a modern format at the right size, mark it `fetchpriority="high"`, and never lazy-load it. Lazy-loading the hero is a classic own goal that makes the metric worse.

The hero image, done correctlyhtml
1<!-- Wrong: the LCP element is deprioritised and delayed -->
2<img src="hero.jpg" loading="lazy">
3
4<!-- Right: eager, prioritised, sized, modern format -->
5<img src="hero.webp" width="1200" height="630" alt="…"
6     loading="eager" fetchpriority="high"
7     srcset="hero-600.webp 600w, hero-1200.webp 1200w"
8     sizes="(max-width: 768px) 100vw, 1200px">
9<!-- → width and height also reserve space, which protects CLS -->
10
11<!-- Fonts: swap so text is visible immediately, preload the one
12     used above the fold -->
13<link rel="preload" href="/inter.woff2" as="font" type="font/woff2" crossorigin>
14<style>@font-face { font-family: Inter; src: url(/inter.woff2); font-display: swap; }</style>

If the LCP element is text, the delay is usually the font. A font loaded without `font-display: swap` leaves text invisible while it downloads, so nothing counts as painted. Swapping to a fallback immediately and letting the web font replace it later trades a small visual shift for a much earlier paint, which is almost always the right call — and preloading the one face used above the fold shrinks the shift too.

What causes layout shift, and how do you stop it?

Anything that changes the size or position of content already on screen. The four repeat offenders are images and videos without dimensions, ads or banners injected above existing content, web fonts with different metrics from the fallback, and content inserted after data loads. Every one has a preventive fix rather than a corrective one, which is what makes CLS the most tractable of the three vitals.

Reserving space so nothing jumpscss
1/* Modern browsers derive the ratio from width/height attributes */
2img { max-width: 100%; height: auto; }
3
4/* For anything you cannot size, reserve the box explicitly */
5.ad-slot     { aspect-ratio: 16 / 9; }
6.skeleton    { min-height: 320px; }
7
8/* Match the fallback's metrics so the font swap barely moves anything */
9@font-face {
10  font-family: "Inter fallback";
11  src: local("Arial");
12  size-adjust: 107%;
13  ascent-override: 90%;
14}
15/* → the swap becomes almost invisible instead of reflowing the page */
16
17/* Toasts and banners: take them out of flow entirely */
18.toast { position: fixed; inset-block-end: 1rem; }

This is 1 of 200+ questions in the Complete Frontend Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the Complete Frontend Kit → ₹499

How do you improve INP and main-thread responsiveness?

INP is a main-thread problem. JavaScript is single-threaded, so while a long task runs nothing else can happen — no click handling, no rendering. Any task over 50ms counts as long, and a page that ships a large bundle can spend seconds parsing and executing before it responds to anything, even though it looks ready.

Breaking up work so the browser can respondjavascript
1// BAD — one long task; the page is frozen throughout
2items.forEach(process);          // 4,000 items, ~600ms
3
4// Yield between chunks so input and paint get a turn
5async function processInChunks(items, size = 100) {
6  for (let i = 0; i < items.length; i += size) {
7    items.slice(i, i + size).forEach(process);
8    await new Promise(r => setTimeout(r, 0));   // yield to the event loop
9  }
10}
11// → six 100ms tasks instead of one 600ms task; clicks land between them
12
13// Or move it off the main thread entirely
14const worker = new Worker("/heavy.js");
15worker.postMessage(items);
16worker.onmessage = (e) => setResult(e.data);
17// → zero main-thread cost; the UI never blocks

The other half of INP is what happens after the click. An interaction is only complete when the next frame is painted, so a handler that finishes quickly but triggers an enormous re-render still scores badly. That is where this topic meets framework performance — and why mentioning that you would check both the handler and the render it causes is a more complete answer than either alone.

How does browser caching work?

`Cache-Control` is the header that matters. The pattern almost every modern site uses is content-hashed filenames for static assets cached for a year and marked immutable, with HTML never cached but revalidated. Because a new deploy produces new filenames, users get the new assets immediately without ever serving a stale one — and nothing needs cache invalidation.

The two-tier pattern, and stale-while-revalidatejavascript
1// Hashed static assets — the filename changes when the content does
2// GET /_next/static/chunks/main-a3f9c1.js
3Cache-Control: public, max-age=31536000, immutable
4// → never revalidated; a deploy changes the URL instead
5
6// HTML — always check, but allow a conditional response
7Cache-Control: no-cache
8// → sends If-None-Match; a 304 costs almost nothing
9
10// API data — serve instantly, refresh in the background
11Cache-Control: public, max-age=60, stale-while-revalidate=600
12// → fresh for 60s; for the next 10 min the stale copy is served
13//   immediately while a new one is fetched behind it

It is worth distinguishing the layers, because interviewers probe it. The browser cache serves one user. A CDN cache serves everyone in a region and is what protects your origin under load. A service worker cache is programmable and is what makes genuine offline support possible. They stack, and knowing which one you would reach for — and that a CDN is what turns a slow TTFB around — is the substance of the answer.

What should you lazy load, and what should you not?

Lazy load anything below the fold and anything behind an interaction: off-screen images, routes the user has not visited, modals, editors, charts, video embeds. Do not lazy load the LCP element, anything visible on first paint, or critical CSS — deferring those makes the metrics worse, which is the trap the question is usually set up to catch.

Native lazy loading, and doing it on intentjavascript
1<img src="thumb.webp" loading="lazy" width="400" height="300" alt="…">
2// → native, no library, no observer to clean up
3
4// Route-level splitting
5const Dashboard = lazy(() => import("./Dashboard"));
6
7// Preload on intent so the wait is hidden behind the user's motion
8<button onMouseEnter={() => import("./RichEditor")}>Edit</button>
9// → the chunk is usually already there by the time they click
10
11// Defer a third-party embed until it is nearly visible
12const io = new IntersectionObserver(([e]) => {
13  if (e.isIntersecting) { loadYouTube(); io.disconnect(); }
14}, { rootMargin: "400px" });
15// → a video embed can be 500KB+; not loading it is the whole win

How would you debug a page that is slow in production?

Start with field data rather than your own machine, because a laptop on office wifi is the least representative device your product has. Search Console and any real-user monitoring you have will tell you which vital is failing, on which devices, in which regions. Only then reproduce it in the lab with throttling that matches — mid-range mobile CPU and a slow connection, not a desktop preset.

  1. Check field data first — which vital is failing, for whom, on what device.
  2. Reproduce with matching throttling: CPU slowdown and a realistic network profile.
  3. Run Lighthouse for a scored summary and the specific opportunities.
  4. Open the Performance panel and look for long tasks and layout thrash.
  5. Check the Network waterfall for render-blocking resources and request chains.
  6. Run a bundle analyser — an oversized dependency is often the whole story.
  7. Fix one thing, measure again, and keep the regression out with a CI budget.

That last step is what turns a fix into an outcome. A performance budget checked in CI — a maximum bundle size, a Lighthouse score floor — is what stops the improvement decaying over the next six months as features land. Mentioning it unprompted signals that you have watched an optimisation get undone, which is a more senior thing to have experienced than any individual technique.

One habit worth carrying into the round: prefer prevention to correction. Reserving space is cheaper than measuring and adjusting, preloading is cheaper than discovering a resource late, and shipping less JavaScript is cheaper than any technique for making a large bundle feel faster. Interviewers notice a candidate whose first instinct is to remove work rather than to schedule it more cleverly, because that is the difference between a page that is fast and a page that has been made to seem fast.

What is layout thrash?

Forcing the browser to recalculate layout repeatedly within one frame by interleaving reads and writes of layout properties. Reading `offsetHeight` or `getBoundingClientRect` after a style change forces a synchronous reflow, because the browser must apply the pending change before it can answer. Do that in a loop and you get one reflow per iteration instead of one per frame.

Interleaved reads and writes versus batched onesjavascript
1// BAD — read, write, read, write: a reflow every iteration
2els.forEach(el => {
3  el.style.height = el.offsetHeight + 10 + "px";
4});
5// → 100 elements = 100 forced synchronous layouts
6
7// GOOD — batch all reads, then all writes
8const heights = els.map(el => el.offsetHeight);   // reads
9els.forEach((el, i) => {                          // writes
10  el.style.height = heights[i] + 10 + "px";
11});
12// → one layout pass
13
14// Animate properties that skip layout and paint entirely
15.card { transition: transform .2s, opacity .2s; }   /* compositor only */
16/* animating width/top/left forces layout on every frame */

What replaced First Input Delay?

Interaction to Next Paint, in March 2024. FID only measured the delay before the first interaction was processed, which flattered pages that responded quickly once and badly thereafter. INP looks at the full latency of interactions across the visit, up to the next paint, so it reflects the whole session.

What is the difference between lab and field data?

Lab data comes from a synthetic run on chosen hardware — Lighthouse — and is reproducible, which makes it good for debugging. Field data comes from real users on their own devices and networks, which is what Google actually ranks on. They frequently disagree, and the field data is the one that matters.

Does image format really matter that much?

Yes. WebP is typically 25–35% smaller than JPEG at equivalent quality and AVIF more again, and images are usually the largest thing on a page. Combined with correct sizing via srcset — serving a 400px image to a 400px slot instead of a 2000px original — it is often the biggest single win available.

How do you measure performance in CI?

Lighthouse CI for scores and vitals against a budget, plus a bundle-size check that fails the build when a limit is exceeded. The point is not the number but the ratchet — without a gate, performance work decays as features land, which is the pattern every team without a budget eventually reports.

Is a service worker worth adding for performance?

For repeat visits and offline support, yes — it can serve a shell instantly and cache API responses. It is not a fix for a slow first load, since it does not control that visit, and it adds real complexity around updates and stale content. Reach for it when offline is a requirement, not as a speed patch.

Frequently asked questions

What are the Core Web Vitals?
Largest Contentful Paint, which measures when the main content appears and should be under 2.5 seconds; Interaction to Next Paint, which measures responsiveness and should be under 200ms; and Cumulative Layout Shift, which measures unexpected movement and should be under 0.1. Google uses all three as ranking signals.
What replaced First Input Delay?
Interaction to Next Paint became a Core Web Vital in March 2024. FID measured only the delay before the first interaction was handled, which flattered pages that were responsive once and sluggish afterwards. INP measures the full latency of interactions throughout the visit.
How do you improve Largest Contentful Paint?
Identify the LCP element first. If it is an image, serve a modern format at the right size, set fetchpriority="high" and never lazy-load it. If it is text, the web font is usually the cause — use font-display: swap and preload the face used above the fold. Also reduce TTFB with a CDN and remove render-blocking resources.
What causes Cumulative Layout Shift?
Images and videos without width and height, ads or banners injected above existing content, web fonts whose metrics differ from the fallback, and content inserted after data loads. All four are preventable by reserving space up front with dimensions, aspect-ratio, or a skeleton of the right size.
What is the difference between defer and async?
Both download in parallel with HTML parsing. defer waits until parsing completes and runs scripts in document order, which makes it right for application code with dependencies. async executes as soon as it downloads, in unpredictable order, which suits only genuinely independent third-party scripts.
What is the critical rendering path?
The sequence from HTML to pixels: parse HTML into the DOM, parse CSS into the CSSOM, combine them into the render tree, compute layout, then paint. Stylesheets and synchronous scripts block it, so minimising and deferring what is in the head is the main lever on first paint.
How does browser caching work?
The Cache-Control header decides. The standard pattern is content-hashed asset filenames cached for a year with immutable, and HTML sent with no-cache so it revalidates. Because a deploy changes the asset URLs, users get new files immediately without any cache invalidation step.
What is stale-while-revalidate?
A Cache-Control directive that lets a cache serve a stale response immediately while fetching a fresh one in the background. The user gets an instant response and the next request gets updated data, which suits API responses where being a few seconds behind is acceptable.
What should you not lazy load?
The LCP element, anything visible on first paint, and critical CSS. Adding loading="lazy" to a hero image is the most common self-inflicted regression, because it tells the browser to deprioritise exactly the resource the metric is measuring.
What is layout thrash?
Forcing repeated synchronous layout calculations by interleaving reads and writes of layout properties within one frame. Reading offsetHeight after a style change forces the browser to apply it immediately. Batch all reads first, then all writes, so only one layout pass is needed.
How do you measure web performance?
Lighthouse for a scored lab run, the Performance panel for long tasks and layout work, the Network waterfall for blocking resources and request chains, and field data from Search Console or real-user monitoring for what users actually experience. Field data is what Google ranks on.
Why do performance numbers differ between my machine and real users?
Because your laptop on fast wifi is the best device your product runs on. Real users are frequently on mid-range Android phones where JavaScript execution — not download — is the bottleneck. Always throttle CPU and network to something representative before drawing conclusions.

This is 1 of 200+ questions in the Complete Frontend Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the Complete Frontend Kit → ₹499
Written by Arun Karthikeyan · Last updated

Full kit

Complete Frontend Kit · ₹499

Get it →