interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

Web Performance Interview Questions and Answers

24 hand-picked Web Performance interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

What are Core Web Vitals and what are the thresholds?

Google's three user-centric metrics, each with a "good" threshold measured at the 75th percentile of real users:

  • LCP — Largest Contentful Paint (loading): time until the largest text block or image in the viewport renders. Good ≤ 2.5 s.
  • INP — Interaction to Next Paint (responsiveness): the worst-case delay between a user interaction and the next visual update, across the whole visit. Good ≤ 200 ms. It replaced FID in March 2024.
  • CLS — Cumulative Layout Shift (visual stability): sum of unexpected layout shift scores. Good ≤ 0.1.

Supporting diagnostics that aren't Core Vitals themselves: TTFB (server response) and FCP (first content painted).

They matter beyond ranking: they're the closest proxy the industry has to "does this site feel fast", and every one of them maps to a specific engineering fix.

How do you improve LCP?

First identify the LCP element (DevTools and Lighthouse both name it) — it's usually a hero image, a heading, or a background image.

LCP breaks into four phases, and each has a different fix:

  1. TTFB — slow server or no CDN. Cache at the edge, reduce server work, use SSG/ISR where possible.
  2. Resource load delay — the browser found out about the image late. <link rel="preload"> or fetchpriority="high" on the hero image; never lazy-load the LCP image.
  3. Resource load time — the image is too big. Modern formats (AVIF/WebP), correct srcset/sizes, compression, CDN resizing.
  4. Element render delay — render-blocking CSS/JS, or content waiting on a client-side fetch. Inline critical CSS, defer non-critical JS, and render the hero server-side.
<!-- hero image: discovered early, fetched first, never lazy -->
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">

<img src="/hero.avif"
     srcset="/hero-800.avif 800w, /hero-1600.avif 1600w"
     sizes="100vw"
     width="1600" height="900"
     fetchpriority="high"
     alt="...">

What is INP and how do you fix a bad one?

Interaction to Next Paint measures the full latency of an interaction — from the click/tap/keypress until the browser paints the visual response — and reports roughly the worst interaction of the visit. Unlike FID, it includes processing time and rendering, not just input delay.

Three parts to attack:

  • Input delay — the main thread was busy when the user clicked. Break up long tasks, defer third-party scripts, avoid heavy work on load.
  • Processing time — your handler does too much. Do the minimum needed to update the UI, and move the rest to a later task or a Web Worker.
  • Presentation delay — rendering the update is expensive: large DOM, expensive layout, huge re-render.

The key technique is yielding: paint the visible response first, then continue the work.

async function onSearch(query) {
  showSpinner();                 // 1. cheap visual feedback
  await yieldToMain();           // 2. let the browser paint
  const results = expensiveFilter(query);   // 3. heavy work after paint
  render(results);
}

function yieldToMain() {
  if (window.scheduler?.yield) return scheduler.yield();
  return new Promise(r => setTimeout(r, 0));
}

What causes layout shift (CLS) and how do you prevent it?

CLS scores unexpected movement of visible content: impact fraction × distance fraction, summed over the session's worst shift windows. Shifts within 500 ms of a user interaction are excluded, since those are expected.

Usual causes and fixes:

  • Images without dimensions — always set width/height (or aspect-ratio) so the browser reserves the box before the file arrives.
  • Ads, embeds and iframes — reserve a min-height container; never let them size themselves after load.
  • Web fonts — a fallback with different metrics reflows text on swap. Use size-adjust/font-display: optional, or a metric-matched fallback.
  • Content injected above existing content — cookie banners, alerts. Overlay them or reserve space.
  • Animating layout properties — animate transform/opacity, never top/height/margin.

What makes up TTFB and how do you reduce it?

Time To First Byte covers everything before your HTML starts arriving: redirects, DNS lookup, TCP connect, TLS handshake, request travel time, server processing, and response travel time.

Reductions, in the order they usually pay off:

  • Eliminate redirect chains — each one is a full round trip, and http→https→www is three.
  • Serve from a CDN edge — physical distance is often the biggest single term.
  • Cache the HTML where you can (SSG/ISR, edge cache with stale-while-revalidate).
  • Cut server work — the N+1 query and the uncached upstream call are the usual suspects.
  • Connection setup — HTTP/2 or /3, TLS 1.3, and preconnect for critical third-party origins.

TTFB isn't a Core Web Vital, but it's a hard floor: LCP can never be faster than TTFB.

Lab data vs field data — why do Lighthouse and real users disagree?

Lab (synthetic) — Lighthouse/WebPageTest run one load on a simulated device and network. Reproducible, great for debugging and CI, but it's a single sample under assumptions you chose.

Field (RUM) — CrUX or your own web-vitals beacons, aggregated over real users, real devices, real networks. This is what Google ranks on and what users actually experience.

Why they disagree: your users have slower phones and worse networks than your test config; real sessions have cached visits, logged-in states and personalisation; Lighthouse can't measure INP properly because there's no real interaction; and field data spans a 28-day rolling window, so a fix takes weeks to show.

Use both: field data tells you whether there's a problem and for whom; lab data tells you why, reproducibly, and guards regressions in CI.

What are render-blocking resources and how do you deal with them?

The browser can't paint until it has built the CSSOM, so every stylesheet in <head> blocks first paint. Synchronous <script> tags block HTML parsing entirely.

CSS: inline the small amount of CSS needed for above-the-fold content, and load the rest asynchronously (media="print" onload="this.media='all'" is the classic trick). Split stylesheets by media so a print or wide-viewport sheet doesn't block.

JavaScript: defer for scripts that need the DOM and must run in order; async for independent third-party scripts; type="module" is deferred by default. Move anything not needed for first paint out of the critical path.

Fonts: preload the one or two fonts used above the fold, and use font-display: swap so text is visible immediately.

How do you code-split an application effectively?

Ship only what the current view needs, and load the rest on demand via dynamic import().

Where to split, by payoff:

  • Route level — the default and biggest win. Every framework router supports it.
  • Heavy components below the fold or behind interaction — charts, editors, maps, modals, date pickers.
  • Large one-off dependencies — a PDF renderer used on one screen shouldn't be in the main bundle.
  • Vendor/common chunks — stable dependencies in their own long-cached chunk.

Don't over-split: dozens of tiny chunks add request overhead and waterfall risk, and a chunk loaded during a click is a spinner the user sees. Prefetch likely-next chunks during idle time (on link hover, or via the router's prefetch) so the split is invisible.

// route-level split
const Reports = lazy(() => import('./pages/Reports'));

// interaction-level split, prefetched on hover
function openEditor() { return import('./editor'); }
button.addEventListener('mouseenter', () => import('./editor'), { once: true });

Your bundle is 2 MB. How do you find and fix the bloat?

Measure first with a visualiser — rollup-plugin-visualizer, webpack-bundle-analyzer, source-map-explorer, or vite-bundle-visualizer. Treemaps make the culprit obvious in seconds.

The usual offenders:

  • A whole library imported for one functionimport _ from 'lodash' instead of lodash-es with named imports.
  • Moment.js locales, or moment at all (use date-fns, dayjs, or Intl).
  • Icon libraries imported wholesale rather than per-icon.
  • Duplicate dependencies — two versions of the same package pulled by different deps.
  • Polyfills for browsers you don't support — check browserslist.
  • Source maps or dev builds accidentally shipped to production.

Then set a size budget in CI so it doesn't creep back.

What is tree shaking and why does it often fail?

Dead-code elimination at bundle time: the bundler statically analyses ES module imports/exports and drops anything never imported.

It requires ESM. CommonJS require() is dynamic and can't be analysed statically, so a CJS-only dependency is included whole.

Why it silently fails:

  • Side effects — if a module might do something on import (register a polyfill, mutate a global), the bundler must keep it. Declare "sideEffects": false (or a file list) in package.json.
  • Namespace importsimport * as utils then dynamic property access defeats analysis.
  • Barrel files — a big index.ts re-exporting everything can drag in modules with side effects.
  • Transpiling ESM to CJS before bundling — a Babel config targeting CommonJS undoes the whole thing.
  • Class methods and property mangling — unused class members generally aren't removed.

How do you optimise images for the web?

Images are usually the largest share of page weight, and the fixes are mechanical:

  • Format — AVIF (smallest) → WebP (universal today) → JPEG/PNG fallback via <picture>. SVG for icons and line art.
  • Right sizesrcset + sizes so a phone doesn't download a 2000 px file. Serving a desktop image to mobile is the most common waste.
  • Lazy-load below the foldloading="lazy"; never on the LCP image.
  • Always set width and height (or aspect-ratio) to prevent CLS.
  • Compress — quality 75–85 is visually indistinguishable for photos at a fraction of the size.
  • Use an image CDN for automatic format negotiation, resizing and caching.
  • Decodingdecoding="async" keeps decode off the critical path.
<picture>
  <source type="image/avif" srcset="/p-400.avif 400w, /p-800.avif 800w" sizes="(max-width:600px) 100vw, 50vw">
  <source type="image/webp" srcset="/p-400.webp 400w, /p-800.webp 800w" sizes="(max-width:600px) 100vw, 50vw">
  <img src="/p-800.jpg" width="800" height="600" loading="lazy" decoding="async" alt="Product">
</picture>

How do you load web fonts without hurting performance?

Fonts are discovered late (CSS → @font-face → download) and cause two visible problems: FOIT (invisible text while loading) and FOUT (text reflows when the real font swaps in).

The playbook:

  • Self-host — third-party font CDNs add a DNS + TLS + request round trip, and cross-site font caching no longer exists.
  • WOFF2 only — every browser that matters supports it.
  • Preload the critical font so it isn't discovered after CSS parses.
  • font-display: swap — show fallback text immediately. Use optional if you'd rather never reflow.
  • Subset — Latin-only subsets are a fraction of the full file; drop unused weights entirely.
  • Match fallback metrics with size-adjust, ascent-override etc. so the swap doesn't shift layout — this is what turns FOUT from a CLS problem into a non-event.
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: swap;
  font-weight: 100 900;          /* variable font: one file, all weights */
}
@font-face {                      /* metric-matched fallback -> no shift on swap */
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
}

How do you design a caching strategy for a web app's assets?

Split assets into two classes and cache them oppositely:

  • Fingerprinted static assets (app.8f3d2a.js, images, fonts) — the URL changes whenever content changes, so cache them forever: Cache-Control: public, max-age=31536000, immutable. immutable also stops revalidation on reload.
  • The HTML entry point — must never be stale, because it references the hashed assets: no-cache (revalidate every time) or a short max-age with stale-while-revalidate at the CDN.

Validators: ETag/Last-Modified let a revalidation return 304 Not Modified — cheap, but still a round trip, which is why hashed assets skip it entirely.

Service worker adds an application-controlled layer: precache the shell, stale-while-revalidate for data, offline fallbacks. Powerful, and the classic footgun — a bad SW can pin users to an old build, so always ship an update path.

What did HTTP/2 and HTTP/3 change for frontend performance?

HTTP/1.1 allowed ~6 connections per origin and suffered head-of-line blocking, which is why we used to concatenate files, inline sprites and shard across domains.

HTTP/2 introduced multiplexing — many streams over one connection — plus header compression (HPACK) and stream priorities. Those old workarounds became counterproductive: domain sharding costs extra connections, and mega-bundles hurt caching granularity. Server push was specified and has since been abandoned; use 103 Early Hints instead.

HTTP/3 moves to QUIC over UDP, removing TCP-level head-of-line blocking (a lost packet no longer stalls unrelated streams), with 0-RTT connection resumption. The gains are largest on lossy mobile networks.

Practically: stop sharding, split bundles for caching rather than concatenating everything, and check your CDN actually serves h2/h3 — it usually just works.

preload, prefetch, preconnect, fetchpriority — when do you use each?

  • preconnect — do the DNS + TCP + TLS handshake to a third-party origin early, before you know the exact URL. Use for 2–3 critical origins only; each one costs a connection.
  • dns-prefetch — DNS only. Cheap fallback for less critical origins.
  • preload — fetch a resource needed for this page at high priority, earlier than the parser would find it. Classic uses: the LCP image, a critical font, a CSS-referenced asset. Must be used within seconds or the browser warns.
  • prefetch — fetch something for a future navigation at lowest priority, during idle time.
  • fetchpriority="high|low" — nudge the priority of a resource the browser already found. High for the hero image, low for below-the-fold or non-critical iframes.

All of them are budget reallocation, not free speed: prioritising everything prioritises nothing.

What is a long task and how do you break one up?

Any main-thread task over 50 ms. While it runs, the browser can't respond to input, run animations, or paint — so long tasks are the direct cause of bad INP and janky scrolling.

Techniques:

  • Yield — split the work into chunks and hand control back between them: scheduler.yield() where available, otherwise setTimeout(…, 0) or await new Promise(queueMicrotask)-style batching.
  • Prioritise with scheduler.postTask() — background priority for work that can wait.
  • Web Workers — move genuinely heavy, self-contained computation (parsing, diffing, crypto, image processing) off the main thread entirely. Cost is structured-clone overhead at the boundary.
  • requestIdleCallback — for non-urgent work like analytics flushes and cache warming.
  • Do less — the real fix is often that you're processing 10,000 rows the user can't see.

Find them in the DevTools Performance panel: long tasks are flagged with a red triangle.

How do you render a list of 50,000 rows?

You don't render them. Virtualise: render only the rows in (or near) the viewport, absolutely position them inside a spacer element sized to the full list height, and swap the rendered window as the user scrolls.

Every DOM node costs memory, style recalculation, layout and paint — a 50k-row table will freeze the tab regardless of framework. Typical libraries: TanStack Virtual, react-window, CDK Virtual Scroll (Angular).

What makes it tricky: variable row heights (need measurement and caching), scroll-anchoring so content doesn't jump, accessibility (screen readers and Ctrl+F only see rendered rows — expose a "show all"/export path), and keeping keyboard navigation working across the virtual boundary.

Alternatives: pagination (simplest, often the right product answer), infinite scroll with windowing, or content-visibility: auto for long static documents — a one-line CSS win that lets the browser skip rendering off-screen sections.

How do you find and fix a memory leak in an SPA?

SPAs leak because the page never reloads, so anything you forget to clean up accumulates across navigations.

Common causes: event listeners on window/document never removed; setInterval never cleared; unsubscribed observables or subscriptions; observers (IntersectionObserver, ResizeObserver, MutationObserver) never disconnected; closures capturing large objects; detached DOM nodes still referenced by a JS variable or a cache; and unbounded in-memory caches or logs.

How to find it:

  1. DevTools → Memory → take a heap snapshot.
  2. Perform the suspect action several times (navigate away and back ×5).
  3. Take another snapshot and compare with Comparison view; sort by delta.
  4. Filter by Detached to find DOM nodes still held alive, and follow the retainer chain to the reference that's keeping them.

The Performance panel's memory track is the quick smoke test: a sawtooth that never returns to baseline is a leak.

Why are transform and opacity the only 'cheap' properties to animate?

The rendering pipeline is style → layout → paint → composite. Which properties you animate decides how much of it re-runs every frame:

  • Animating width, height, top, margin, font-size triggers layout — then paint, then composite. Most expensive, and it can invalidate layout for large parts of the tree.
  • Animating background-color, box-shadow, border-radius skips layout but triggers paint.
  • Animating transform and opacity can be handled entirely by the compositor, often on the GPU and off the main thread — so they stay smooth even when JavaScript is busy.

Tools: promote a layer with will-change: transform just before animating and remove it after — leaving it on permanently wastes GPU memory. The Web Animations API and CSS animations both run on the compositor when the properties allow.

How do you manage the performance cost of third-party scripts?

Third-party scripts (tag managers, analytics, chat widgets, ads, A/B tools) are usually the largest performance cost you don't control — and they often inject more scripts at runtime, so their real weight is invisible in your bundle.

Tactics:

  • Inventory and justify — measure each one's cost and get an owner to defend it. Many are forgotten from campaigns years ago.
  • Load lateasync/defer, or after first interaction/idle. Almost nothing needs to run before first paint.
  • Facades — render a lightweight placeholder (a chat button image, a video thumbnail) and load the real widget on click. Huge win for embeds.
  • Self-host stable scripts where the licence allows, to cut a connection and control caching.
  • Isolate — Partytown can move some third-party scripts into a Web Worker; sandboxed iframes limit blast radius.
  • Monitor — alert on third-party script size and long tasks; they change without telling you.

What is a performance budget and how do you enforce it?

A hard limit agreed up front, so performance is a build failure rather than a quarterly cleanup.

Three kinds: quantity (JS ≤ 170 KB compressed, ≤ 1 MB images per page), timing (LCP ≤ 2.5 s on a mid-tier phone, TBT ≤ 200 ms), and score (Lighthouse ≥ 90) — quantity budgets are the most actionable because a developer can act on them immediately.

Enforcement: bundlesize/size-limit on bundle output, Lighthouse CI on preview deployments with assertions, and a bot comment on the PR showing the delta versus main.

What makes it stick: fail the build (a warning is ignored within a sprint), report the diff rather than the absolute number so the responsible PR is obvious, and allow explicit, reviewed exceptions rather than pretending they'll never be needed.

How do you profile a slow page in DevTools?

Set up realistically first: CPU throttling 4–6×, network throttling to Slow 4G, and an incognito window (extensions distort everything). Testing on an unthrottled desktop is why "it's fast on my machine".

Panels and what each answers:

  • Performance — record a load or interaction. The flame chart shows what ran; long tasks are flagged red; the Main track shows scripting vs rendering vs painting. This is where you find why it's slow.
  • Network — waterfall, request priority, sizes, and blocking chains. Look for serial dependencies (a fetch that only starts after another finishes).
  • Coverage — unused CSS/JS bytes on load.
  • Lighthouse — an opinionated summary and a starting checklist, not a diagnosis.
  • Memory — heap snapshots for leaks.
  • Framework profilers (React DevTools Profiler, Angular DevTools) — component-level render costs, which the browser can't attribute for you.

What is hydration and why is it a performance problem?

Server-rendered HTML paints fast, but it's inert. Hydration is the client re-running the component tree to attach event listeners and rebuild state so the markup becomes interactive.

The cost is the uncanny valley: content is visible but nothing responds — clicks are dropped and INP suffers — and you effectively pay for the UI twice (rendered on the server, re-executed on the client, with the component code shipped anyway).

Mitigations, in increasing order of ambition:

  • Streaming SSR + progressive hydration — hydrate above-the-fold and interactive parts first.
  • Islands architecture (Astro) — ship JS only for genuinely interactive components; the rest stays static HTML.
  • React Server Components — components that never ship to the client at all.
  • Resumability (Qwik) — serialise listener state into the HTML so no re-execution is needed.

How do you make navigation between pages feel instant?

  • Prefetch on intent — fetch the next route's code and data on link hover or touchstart. By the time the click lands, it's cached. Most frameworks' routers do this for links in the viewport.
  • Speculation Rules API — declaratively tell the browser to prefetch or even prerender likely next pages, including for MPAs.
  • bfcache — back/forward navigation restores the whole page instantly from memory. You break it with unload handlers, Cache-Control: no-store, or open IndexedDB transactions; DevTools' Back/forward cache panel tells you why it failed.
  • Optimistic UI — render the new view's shell and skeletons immediately rather than blocking on data.
  • Keep data cached — a client cache (TanStack Query, RTK Query) makes revisiting a page instant with a background refresh.
  • View Transitions API — smooth cross-document/DOM transitions that hide the remaining latency.