Web Performance and Core Web Vitals Interview Questions: 10 Questions with Clear Answers
In front-end technical interviews, web performance and Core Web Vitals are core evaluation criteria. Beyond basic asset bundling, interviewers test your understanding of browser rendering pipelines, interaction latency, and real-user metrics.
This guide covers 10 high-frequency Web Performance and Core Web Vitals interview questions for developers with 0–5 years of experience.
Question Index
| # | Question | Focus Area | Difficulty |
|---|---|---|---|
| 1 | Core Web Vitals Metrics and Thresholds | LCP, INP, CLS definitions and targets | Junior |
| 2 | Diagnosing and Optimizing LCP | LCP subparts, resource priority, images | Junior-Mid |
| 3 | Interaction to Next Paint (INP) vs FID | Event duration, input delay, main thread | Mid |
| 4 | Preventing Cumulative Layout Shift (CLS) | Aspect ratios, dynamic injections, fonts | Mid |
| 5 | Eliminating Layout Thrashing | DOM read/write batching and reflows | Mid |
| 6 | Breaking Long Tasks & Main Thread Scheduling | 50ms threshold, scheduler.yield() | Mid-Senior |
| 7 | Script Loading: async, defer, type='module' | Parser blocking, execution timing | Junior-Mid |
| 8 | Resource Hints & fetchpriority | Network priority queue and preload | Mid |
| 9 | Field Monitoring with PerformanceObserver | Programmatic RUM tracking | Mid-Senior |
| 10 | Optimizing React Transitions and INP | State transitions, useTransition | Mid-Senior |
Q1. What are Google's Core Web Vitals, what thresholds define a "Good" user experience, and what does each metric measure?
Level: Junior
What the interviewer is checking: Foundational knowledge of Google's user-centric performance metrics, their official targets at the 75th percentile of field data, and what aspect of page experience each measures.
Short answer to say out loud
"Core Web Vitals are three user-centric metrics evaluated at the 75th percentile of real users: LCP measures loading speed (good <= 2.5s), INP measures interaction responsiveness across the entire visit (good <= 200ms), and CLS measures visual stability (good score <= 0.1)."
Detailed explanation
Core Web Vitals measure three pillars of user experience:
- LCP (Loading): Tracks render time of the largest visible content block. Replaces FCP as the primary loading indicator.
- INP (Interactivity): Replaced FID in March 2024. Captures the worst latency across all clicks, taps, and keypresses during a session.
- CLS (Stability): Calculates unexpected layout shifts:
shift score = impact fraction * distance fraction. User-initiated shifts within 500ms are excluded.
Example
const THRESHOLDS = {
LCP: 2500, // ms (good <= 2.5s)
INP: 200, // ms (good <= 200ms)
CLS: 0.1 // score (good <= 0.1)
};
function isGoodVital(metric, value) {
return value <= THRESHOLDS[metric];
}
console.log(isGoodVital('LCP', 2100)); // true (Good)
console.log(isGoodVital('INP', 180)); // true (Good)
console.log(isGoodVital('CLS', 0.04)); // true (Good)
Common mistakes
- Stating that FID is still a Core Web Vital. FID was deprecated and officially replaced by INP in March 2024.
- Assuming metrics are evaluated on page averages rather than the 75th percentile of real-user distributions.
- Confusing LCP with
window.onloador DOMContentLoaded. LCP measures visual paint of the largest viewport element, not document parsing completion.
Likely follow-up questions
- Interviewer: "Why does Google evaluate metrics at the 75th percentile rather than the median (50th) or 95th percentile?"
- Answer: "The 75th percentile ensures that at least 3 out of 4 users have a good experience while filtering out extreme statistical outliers (like catastrophic device crashes) that would distort the 95th percentile."
Q2. How do you diagnose and optimize Largest Contentful Paint (LCP) in a web application?
Level: Junior-Mid
What the interviewer is checking: Understanding the 4 subparts of LCP, eliminating render-blocking resources, and prioritizing above-the-fold assets.
Short answer to say out loud
"LCP consists of four subparts: TTFB, Resource Load Delay, Resource Load Duration, and Element Render Delay. To optimize LCP, serve HTML quickly via CDNs, ensure the LCP image is directly discoverable in initial HTML, preload it with fetchpriority='high', and never use loading='lazy' on hero images."
Detailed explanation
LCP consists of four subparts:
- TTFB: Server response time (target < 800ms).
- Resource Load Delay: Time before the LCP asset is requested (target < 250ms, ideally 0ms).
- Resource Load Duration: Network download time (optimized with WebP/AVIF and compression).
- Element Render Delay: Delay between download completion and visual paint (delayed by hydration or main thread congestion).
Example
<!-- In <head>: discover and preload hero asset early -->
<link
rel="preload"
as="image"
href="/hero-800.webp"
imagesrcset="/hero-400.webp 400w, /hero-800.webp 800w"
fetchpriority="high"
/>
<!-- In <body>: render with explicit dimensions and high priority -->
<img
src="/hero-800.webp"
alt="Dashboard preview"
width="800"
height="450"
fetchpriority="high"
decoding="async"
/>
Common mistakes
- Adding
loading="lazy"to the hero/LCP image, which delays request dispatch until the browser executes layout and calculates viewport intersection. - Loading the hero image via CSS
background-image: url(...). Browsers cannot request CSS background images until external stylesheets are downloaded and parsed.
Likely follow-up questions
- Interviewer: "What is the impact of client-side hydration on LCP in single-page applications?"
- Answer: "In pure SPAs, the LCP asset cannot even be requested until client JavaScript bundles download, parse, and execute to render the image tag. Server-side rendering (SSR) or static site generation (SSG) delivers the image tag directly in the initial HTML, eliminating hundreds of milliseconds of resource load delay."
Q3. How does Interaction to Next Paint (INP) work, and how does it differ from First Input Delay (FID)?
Level: Mid
What the interviewer is checking: Comprehension of full-session interaction latency, the three phases of an interaction, and why FID was inadequate.
Short answer to say out loud
"FID only measured the input delay (how long the user waited for the browser to start processing their very first click) and ignored event handler execution and rendering time. In contrast, INP measures the total latency of all qualifying user interactions (clicks, taps, keypresses) throughout the entire session. An interaction's latency consists of three phases: Input Delay, Processing Duration, and Presentation Delay. INP reports the longest interaction latency, ensuring the UI reliably updates within 200ms."
Detailed explanation
An interaction consists of three sequential phases:
- Input Delay: Waiting for the main thread to begin running event handlers.
- Processing Duration: Synchronous JavaScript execution inside event callbacks.
- Presentation Delay: Time needed for style recalculation, layout, and painting the next frame. FID only measured input delay on the first interaction, ignoring handler execution and presentation delay. INP measures total latency across all three phases for all interactions.
Example
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.interactionId) continue;
const inputDelay = entry.processingStart - entry.startTime;
const processingDuration = entry.processingEnd - entry.processingStart;
const presentationDelay = entry.duration - (entry.processingEnd - entry.startTime);
console.log({ type: entry.name, inputDelay, processingDuration, presentationDelay });
}
}).observe({ type: 'event', durationThreshold: 40, buffered: true });
Common mistakes
- Believing scrolling or hovering counts toward INP. Scrolling and zooming are continuous interactions handled by the compositor thread and are excluded from INP.
- Assuming INP only measures JavaScript execution time. Presentation delay (re-rendering large DOM trees after handlers finish) often accounts for over half of poor INP scores.
Likely follow-up questions
- Interviewer: "How does INP handle outlier interactions on pages where users click hundreds of times?"
- Answer: "On pages with high interaction volume, reporting the absolute worst interaction would be skewed by occasional system spikes. For every 50 interactions on a page, the browser ignores one highest interaction outlier, ensuring the 75th percentile reflects genuine experience."
Q4. What causes Cumulative Layout Shift (CLS), and how do you systematically prevent layout instability?
Level: Mid
What the interviewer is checking: Understanding visual instability causes (images, fonts, dynamic DOM injection, ads) and technical solutions like aspect-ratio reserving, font-display, and containment.
Short answer to say out loud
"CLS quantifies unexpected layout movement of visible elements. To prevent it, reserve dimensional space on images with explicit width/height or CSS aspect-ratio, reserve container space with min-height for dynamic banners, and match fallback font metrics with CSS size-adjust."
Detailed explanation
Layout shifts occur when elements change their start position after rendering:
- Missing dimensions: Images without
widthandheightrender with 0px height initially and push content down once loaded. Setting dimensions allows the browser to calculateaspect-ratioupfront. - Font re-wrapping: Fallback fonts with mismatched metrics cause text re-wrap when web fonts swap. CSS
size-adjustandascent-overridematch fallback dimensions. - Dynamic injections: Banners or ads injected into the document flow shift content unless space is reserved with
min-height.
Example
/* Reserve aspect ratio space for media to prevent layout shift */
.media-wrapper {
width: 100%;
aspect-ratio: 16 / 9;
}
/* Reserve minimum height for dynamically injected banners */
.banner-slot {
min-height: 120px;
contain: layout;
}
/* Match font fallback metrics to eliminate shift on webfont load */
@font-face {
font-family: 'Fallback-Inter';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
}
Common mistakes
- Thinking layout shifts that occur immediately after user clicks count toward CLS. Shifts occurring within 500ms of a discrete user gesture have
hadRecentInput === trueand are excluded from CLS. - Setting
width: 100%on images without settingheight: autoor an explicitaspect-ratio.
Likely follow-up questions
- Interviewer: "How does CSS
contain: layoutorcontain: stricthelp with rendering stability and performance?" - Answer: "
contain: layouttells the browser that internal layout changes inside the container never affect outside ancestor or sibling elements, confining reflow calculations to that subtree and preventing cascading layout shifts."
Q5. What is layout thrashing (forced synchronous layout), and how do you eliminate it in performance-critical code?
Level: Mid
What the interviewer is checking: Understanding the browser's render pipeline (JavaScript -> Style -> Layout -> Paint -> Composite) and how interleaved DOM reads and writes cause forced reflows.
Short answer to say out loud
"Layout thrashing, or forced synchronous layout, occurs when JavaScript interleaves DOM write operations (like changing styles or appending elements) with DOM read operations (like reading offsetHeight, getBoundingClientRect(), or scrollTop). The browser queues style changes lazily, but when JavaScript immediately requests a geometric property, the browser is forced to synchronously recalculate styles and re-run layout on the spot. To eliminate it, batch all DOM reads first, then batch all DOM writes, or schedule writes inside requestAnimationFrame."
Detailed explanation
The browser lazily batches style calculations. However, interleaving DOM mutations (writes) with geometric property reads (offsetWidth, getBoundingClientRect()) forces immediate synchronous layout calculations on every iteration. Batching reads first and writes second eliminates redundant layouts.
Example
// Bad: Interleaved read/write triggers layout thrashing
// elements.forEach(el => { el.style.height = `${el.offsetWidth * 0.75}px`; });
// Good: Batch reads first, then batch writes in requestAnimationFrame
function resizeItems(elements) {
const widths = elements.map((el) => el.offsetWidth); // Batch reads
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.height = `${widths[i] * 0.75}px`; // Batch writes
});
});
}
Common mistakes
- Reading geometric properties inside scroll event listeners without throttling or decoupling reads via
IntersectionObserver. - Assuming modern JavaScript engines automatically optimize interleaved DOM reads and writes. JavaScript cannot bypass synchronous layout when explicit geometry is requested after a mutation.
Likely follow-up questions
- Interviewer: "Why is
IntersectionObserverpreferred overgetBoundingClientRect()in scroll handlers?" - Answer: "
IntersectionObservercomputes element visibility asynchronously during the browser's native compositing step off the main thread, whereas callinggetBoundingClientRect()inside a scroll handler forces synchronous layout calculation on every tick."
Q6. How do you identify long tasks on the main thread, and what strategies exist to break them up?
Level: Mid-Senior
What the interviewer is checking: Understanding the 50ms long task threshold, main thread starvation, scheduler.yield(), micro-yielding, and background web workers.
Short answer to say out loud
"A Long Task is any synchronous JavaScript execution exceeding 50ms on the main thread, which blocks user input and inflates INP. To break up long tasks, offload heavy computation to Web Workers, slice loops, and yield to the main thread using scheduler.yield() or setTimeout."
Detailed explanation
Any task exceeding 50ms is classified as a Long Task. Tasks blocking the main thread prevent the browser from responding to user interactions, degrading INP.
scheduler.yield(): Pauses task execution to allow the browser to paint and handle queued input before resuming.setTimeout(..., 0): Macrotask fallback that yields control to the event loop.- Web Workers: Offload heavy computational work entirely off the main thread.
Example
async function processChunked(items, fn) {
let start = performance.now();
for (const item of items) {
fn(item);
if (performance.now() - start > 16) {
if ('scheduler' in window && 'yield' in window.scheduler) {
await window.scheduler.yield();
} else {
await new Promise((r) => setTimeout(r, 0));
}
start = performance.now();
}
}
}
Common mistakes
- Using
Promise.resolve().then(...)to yield. Promises schedule microtasks, which run synchronously before the browser can yield to rendering or handle user inputs. Only macrotasks orscheduler.yield()allow rendering. - Offloading trivial tasks to Web Workers where the serialization overhead of
postMessageexceeds the task execution time.
Likely follow-up questions
- Interviewer: "Why doesn't
requestAnimationFramework well for task slicing?" - Answer: "
requestAnimationFrameexecutes immediately before the browser paints the frame. Using it to run heavy chunked calculations will delay the paint of that frame rather than yielding after it."
Q7. What are the differences between script loading attributes (standard, async, defer, type='module') on the critical rendering path?
Level: Junior-Mid
What the interviewer is checking: Understanding how external JavaScript affects HTML parser blocking, execution order, and the DOMContentLoaded event.
Short answer to say out loud
"A standard <script src='...'> blocks HTML parsing while the script is downloaded and executed immediately.
defer: Downloads asynchronously in parallel with HTML parsing, but executes only after parsing is complete, strictly maintaining document order beforeDOMContentLoaded.async: Downloads in parallel with HTML parsing, but pauses parsing to execute immediately as soon as it finishes downloading, with no guaranteed execution order.type='module': Defers execution by default (likedefer), executes in document order after parsing, and supports ES modules."
Detailed explanation
Script attributes determine parser interaction:
- Normal script: Halts HTML parsing while downloading and executing synchronously.
defer: Downloads in parallel with parsing; executes after parsing in DOM order beforeDOMContentLoaded.async: Downloads in parallel; pauses parser to execute immediately upon completion.type='module': Deferred by default, maintaining module scoping.
Example
<!-- Critical UI Application Code: Defer preserves order & doesn't block parser -->
<script src="/vendor.js" defer></script>
<script src="/app.js" defer></script>
<!-- Independent Third-Party Analytics: Async loads without blocking -->
<script src="https://analytics.example.com/tracker.js" async></script>
<!-- Modern ES Module: Deferred by default, scoped strictly to module -->
<script type="module" src="/main.js"></script>
Common mistakes
- Putting
deferon inline script tags.deferhas no effect on inline<script>tags that lack asrcattribute. - Using
asyncfor dependent scripts (e.g. jQuery and a jQuery plugin), causing race conditions where the plugin executes before the base library finishes loading.
Likely follow-up questions
- Interviewer: "Does
DOMContentLoadedwait forasyncscripts?" - Answer: "No.
DOMContentLoadedwaits for HTML parsing and alldeferscripts to finish, but anasyncscript may execute before or afterDOMContentLoadeddepending on network timing."
Q8. How do resource hints (preload, preconnect, dns-prefetch) and fetchpriority optimize network utilization?
Level: Mid
What the interviewer is checking: Network optimization techniques, resource prioritization queues in Chromium, and preventing bandwidth saturation.
Short answer to say out loud
"preconnect performs DNS lookup and TLS handshakes for external origins ahead of time, while preload triggers immediate downloads of late-discovered critical resources like fonts. fetchpriority='high' instructs the browser to prioritize the resource concurrently with critical styles and scripts."
Detailed explanation
Browsers prioritize requests by resource type and location:
preconnect: Resolves DNS and completes TCP/TLS handshakes for critical third-party origins ahead of time.preload: Downloads late-discovered critical resources (like fonts or hero images) immediately.fetchpriority: Overrides browser heuristics, boosting LCP images to 'high' priority concurrently with critical scripts.
Example
<head>
<!-- 1. Warm connection to critical external CDN origin -->
<link rel="preconnect" href="https://assets.my-cdn.com" crossorigin />
<!-- 2. Preload critical web font with correct CORS matching -->
<link
rel="preload"
href="/fonts/inter-bold.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- 3. Lower priority on below-the-fold or non-critical scripts -->
<script src="/secondary-feature.js" fetchpriority="low" async></script>
</head>
Common mistakes
- Preloading fonts without the
crossorigin="anonymous"attribute. Web fonts are fetched via anonymous CORS mode; omittingcrossoriginresults in the browser downloading the font file twice (once for preload and once for the CSS font-face). - Overusing
preload. Preloading dozens of non-critical assets starves the network connection of bandwidth needed for critical render-blocking CSS and HTML.
Likely follow-up questions
- Interviewer: "How does
prefetchdiffer frompreload?" - Answer: "
preloaddownloads a resource needed for the current page with high urgency.prefetchdownloads a resource needed for subsequent navigation with lowest priority when the browser is idle."
Q9. How do you implement programmatic Real-User Monitoring (RUM) for Core Web Vitals using the PerformanceObserver API?
Level: Mid-Senior
What the interviewer is checking: Practical implementation of performance monitoring in JavaScript, dealing with buffered entries, and reporting metrics safely off the main thread.
Short answer to say out loud
"Use the native PerformanceObserver API to observe performance entry types: largest-contentful-paint for LCP, event for INP, and layout-shift for CLS. Set buffered: true so the observer catches events that occurred before the observer script initialized. Accumulate values according to metric specs, and transmit data to an analytics endpoint using navigator.sendBeacon() or fetch(..., { keepalive: true }) inside the visibilitychange event listener."
Detailed explanation
Real-User Monitoring (RUM) tracks field data via PerformanceObserver:
- LCP: Observes
largest-contentful-paintentries until page hide or user interaction. - CLS: Sums
layout-shiftentries that lackhadRecentInput. - Delivery: Telemetry is sent via
navigator.sendBeacon()duringvisibilitychange(hidden), preserving bfcache eligibility.
Example
// Lightweight field monitoring for LCP and CLS
let lcp = 0;
let cls = 0;
new PerformanceObserver((list) => {
const entries = list.getEntries();
lcp = entries[entries.length - 1].startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true });
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) cls += entry.value;
}
}).observe({ type: 'layout-shift', buffered: true });
// Transmit on visibilitychange (preserves bfcache)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', JSON.stringify({ lcp: Math.round(lcp), cls }));
}
});
Common mistakes
- Relying on
window.onbeforeunloadorwindow.onunloadto transmit telemetry. Modern browsers discard standard fetch requests during teardown and deprecateunloadbecause it prevents the Back/Forward Cache (bfcache). - Omitting
buffered: trueonPerformanceObserver, which causes the observer to miss LCP entries that painted prior to script execution.
Likely follow-up questions
- Interviewer: "What is the Back/Forward Cache (bfcache), and how does it impact Core Web Vitals?"
- Answer: "The bfcache saves a complete snapshot of the page (including JavaScript heap) in memory so back/forward navigations restore instantly with near-zero LCP. Listening to the
unloadevent prevents the browser from using bfcache, whereaspagehideandvisibilitychangepreserve bfcache eligibility."
Q10. How do you optimize React re-rendering performance to directly improve Interaction to Next Paint (INP)?
Level: Mid-Senior
What the interviewer is checking: Connecting React-specific optimization patterns (transitions, memoization, virtualization, deferred rendering) to browser-level INP metrics.
Short answer to say out loud
"High INP in React stems from expensive synchronous component re-renders delaying browser paint after user interactions. Optimize INP by separating urgent updates from non-urgent state changes using useTransition, virtualizing large lists, and avoiding expensive synchronous layout reads in useLayoutEffect."
Detailed explanation
Expensive synchronous React re-renders inside event handlers block the main thread and inflate INP presentation delay.
useTransition: Separates urgent updates (input feedback) from non-urgent state changes (filtering lists), allowing the browser to paint immediately.- Virtualization: Renders only DOM nodes currently visible in the viewport, preventing massive layout and style recalculations.
useDeferredValue: Defers non-critical child rendering until higher-priority updates finish.
Example
import { useState, useTransition, ChangeEvent } from 'react';
export function FilterSearch({ items }: { items: string[] }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState<string[]>(items);
const [isPending, startTransition] = useTransition();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setQuery(val); // Urgent: immediate input response
startTransition(() => {
// Non-urgent: yields to browser paint
setFiltered(items.filter((item) => item.toLowerCase().includes(val.toLowerCase())));
});
};
return (
<div>
<input value={query} onChange={handleChange} placeholder="Search..." />
{isPending && <span>Updating list...</span>}
<ul>{filtered.slice(0, 50).map((i) => <li key={i}>{i}</li>)}</ul>
</div>
);
}
Common mistakes
- Wrapping the input value state update itself in
startTransition. That causes the typed text to feel laggy. Only the expensive downstream computation/render should be wrapped instartTransition. - Prematurely micro-optimizing primitive components with
useMemowhile ignoring massive un-virtualized DOM trees that dominate render time.
Likely follow-up questions
- Interviewer: "What is the difference between
useTransitionand debouncing?" - Answer: "Debouncing artificially delays code execution by an arbitrary timer (e.g. 300ms) regardless of machine speed.
useTransitionexecutes immediately but adaptively yields to higher-priority user inputs and paints, giving fast devices instant updates while keeping slower devices responsive."
Preparation Checklist
Review this actionable preparation checklist before your interview:
- Memorize Core Web Vitals thresholds: LCP (<= 2.5s), INP (<= 200ms), CLS (<= 0.1) at the 75th percentile.
- Explain INP vs FID: Clarify why full-session latency tracking replaced first-interaction input delay.
- Optimize LCP hero assets: Preload with
fetchpriority='high'and avoidloading='lazy'on above-the-fold images. - Prevent layout shifts: Specify image
aspect-ratioand reserve space for dynamic banners withmin-height. - Eliminate layout thrashing: Batch DOM reads before writes and decouple scroll geometry with
IntersectionObserver. - Break up long tasks: Slice tasks exceeding 50ms using
scheduler.yield(). - Master script loading: Know
defer(preserves order) vsasync(executes immediately upon load). - Track field vitals: Observe entries via
PerformanceObserverand dispatch vianavigator.sendBeacon().
Conclusion
Web performance and Core Web Vitals are critical engineering requirements that directly impact user retention and business metrics. Mastering browser rendering pipelines, interaction latency phases, and resource scheduling allows you to demonstrate technical depth and production readiness in front-end interviews.