Loading challenge...
Preparing the challenge details...
Loading challenge...
Preparing the challenge details...
Core Web Vitals (LCP, INP, CLS), loading strategies (code splitting, tree shaking, resource hints, image optimization), runtime performance (layout thrashing, compositor animations, web workers, virtualization), and a structured debugging framework.
This round overlaps heavily with web fundamentals — the rendering pipeline, reflow/repaint, and the critical rendering path are the foundation here. The new ground is Core Web Vitals, loading strategies, runtime performance, and a clean "how would you debug a slow page" answer.
Google's three user-centric metrics. The big update to know: INP replaced FID in March 2024 — if you say FID, you'll sound a year out of date.
| Metric | Measures | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading — when the biggest visible element renders | ≤ 2.5s | 2.5–4s | > 4s |
| INP (Interaction to Next Paint) | Responsiveness — latency across all interactions | ≤ 200ms | 200–500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability — how much content jumps around | ≤ 0.1 | 0.1–0.25 | > 0.25 |
The render time of the largest image or text block in the viewport. Usually a hero image, a big heading, or a banner.
What makes it bad: slow server response (high TTFB), render-blocking CSS/JS, large unoptimized images, client-side rendering that delays the main content, lazy-loading the hero image (a classic mistake).
How to fix:
srcset.<link rel="preload"> the LCP image so it's fetched early.loading="lazy" the hero — that delays the very thing LCP measures.HTMLindex.html1<!-- Tell the browser the hero matters, fetch it ASAP --> 2<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" /> 3<img src="/hero.avif" fetchpriority="high" alt="..." /> <!-- never loading="lazy" here -->
Replaced FID. Where FID only measured the delay before the first interaction's handler started, INP measures the full latency (input delay + processing + next paint) across every interaction during the page's life, then reports a near-worst-case value. Much harder to game.
What makes it bad: long-running JavaScript on the main thread blocking the response to a click/tap, large re-renders, heavy synchronous work in event handlers.
How to fix:
setTimeout, requestIdleCallback, or scheduler.yield().useTransition / useDeferredValue to mark non-urgent updates so urgent interactions stay responsive.JSfile.js1// yield to the browser so a long loop doesn't freeze interactions 2async function processInChunks(items) { 3 for (let i = 0; i < items.length; i++) { 4 doWork(items[i]); 5 if (i % 100 === 0) await new Promise((r) => setTimeout(r, 0)); // let the UI breathe 6 } 7}
Sums up how much visible content unexpectedly moves. The annoying "I went to tap a button and an ad loaded and pushed it down" feeling.
What makes it bad: images/videos without dimensions, ads/embeds/iframes injected without reserved space, web fonts causing FOIT/FOUT reflow, content inserted above existing content.
How to fix:
width/height (or aspect-ratio) on images and media so the browser reserves space before they load.font-display: optional / swap and preload fonts to limit text reflow.CSSstyles.css1img { aspect-ratio: 16 / 9; width: 100%; height: auto; } /* reserves layout space */
Field vs lab data — a point worth raising: CrUX/RUM (real users) is "field" data; Lighthouse is "lab" data (one synthetic run). They diverge because real users have varied devices and networks. Both matter; Core Web Vitals scoring uses field data.
Splitting the bundle so users download only what they need.
JSXcomponent.jsx1// route-based split with React.lazy + Suspense 2const Dashboard = lazy(() => import('./Dashboard')); 3 4<Suspense fallback={<Spinner />}> 5 <Dashboard /> 6</Suspense>
Dead-code elimination at build time. The bundler drops exports you never import. Requires ES modules (static import/export) since they're statically analyzable, unlike CommonJS require. Import named members (import { debounce } from 'lodash-es') rather than the whole library to keep it effective.
| Hint | Purpose | When |
|---|---|---|
preload | Fetch a resource needed for this page, with high priority | LCP image, critical font, key script |
prefetch | Fetch a resource likely needed for the next navigation, low priority | Next-page bundle on hover/idle |
preconnect | Warm up the connection (DNS + TCP + TLS) to a third-party origin early | API domain, CDN, fonts host |
dns-prefetch | Just the DNS lookup — lighter than preconnect | Many third-party origins |
HTMLindex.html1<link rel="preconnect" href="https://api.example.com" /> 2<link rel="preload" as="font" href="/inter.woff2" type="font/woff2" crossorigin /> 3<link rel="prefetch" href="/checkout.chunk.js" />
The single biggest payload on most pages. The toolkit:
srcset + sizes so mobile doesn't download a desktop-sized image.loading="lazy" for below-the-fold images (but never the LCP/hero image).decoding="async" so image decode doesn't block rendering.HTMLindex.html1<img 2 src="/photo-800.jpg" 3 srcset="/photo-400.jpg 400w, /photo-800.jpg 800w, /photo-1200.jpg 1200w" 4 sizes="(max-width: 600px) 400px, 800px" 5 loading="lazy" 6 decoding="async" 7 width="800" height="600" 8 alt="room" />
Reading a layout property (offsetHeight, getBoundingClientRect, scrollTop) forces the browser to flush pending layout changes synchronously. Interleaving reads and writes in a loop causes layout thrashing.
JSfile.js1// BAD — read forces sync layout every iteration 2items.forEach((el) => { el.style.height = el.offsetHeight * 2 + 'px'; }); 3 4// GOOD — all reads first, then all writes 5const heights = items.map((el) => el.offsetHeight); // batch reads 6items.forEach((el, i) => { el.style.height = heights[i] * 2 + 'px'; }); // batch writes
transform and opacity animations skip layout and paint and run on the GPU compositor thread — that's how you get a steady 60fps. Animating top/left/width/height triggers reflow every frame and stutters.
CSSstyles.css1/* smooth — compositor only */ 2.modal { transition: transform 0.2s, opacity 0.2s; } 3 4/* janky — reflows every frame */ 5.modal { transition: top 0.2s, height 0.2s; }
will-change: transform can hint the browser to promote an element to its own layer ahead of time — use sparingly, as too many layers cost memory.
Schedule visual updates to run right before the next paint, synced to the display's refresh rate. Use it for animations instead of setTimeout, and to batch DOM writes.
JSfile.js1function animate() { 2 el.style.transform = `translateX(${x}px)`; 3 x += 2; 4 if (x < 300) requestAnimationFrame(animate); 5} 6requestAnimationFrame(animate);
JS is single-threaded; a heavy computation (parsing, sorting huge data, image processing) on the main thread freezes the UI and tanks INP. Offload it to a worker, which runs on a separate thread and messages back.
JSfile.js1// main thread 2const worker = new Worker('parser.js'); 3worker.postMessage(largeDataset); 4worker.onmessage = (e) => render(e.data); // UI stays responsive while worker runs
Render only the rows currently in the viewport (plus a small buffer) instead of thousands of DOM nodes. react-window / react-virtualized. It keeps the DOM node count constant regardless of list size, so scroll stays smooth and memory stays flat.
app.a3f9c.js) and Cache-Control: max-age=31536000, immutable. The hash changes when content changes, so you cache forever and bust automatically on deploy.no-cache so users get the new asset references promptly.Serve static assets from edge servers geographically close to users — cuts latency dramatically and offloads your origin. Pair with the immutable-hashed-asset pattern.
Server negotiates via Accept-Encoding / Content-Encoding. Compression on text assets is one of the cheapest big wins.
Tree shaking, code splitting, swapping heavy dependencies for lighter ones (e.g. date-fns over moment), dynamic imports for rarely-used features, and analyzing with a bundle analyzer to find bloat.
Have this ready as a structured walkthrough. The key principle to lead with: measure first, optimize second — never guess.
1. Identify the symptom. Slow load or slow interaction? They point to different tools and fixes.
2. Measure with the right tools:
3. Diagnose against the metric:
4. Fix the biggest bottleneck, then re-measure. Optimization is iterative — fix one thing, measure again, confirm it actually helped before moving on. Avoid premature micro-optimizations that don't move the needle.
Core Web Vitals: LCP (≤2.5s, loading — preload hero, optimize images, never lazy-load it), INP (≤200ms, responsiveness — replaced FID in 2024, break up long tasks, use workers), CLS (≤0.1, stability — set image dimensions, reserve space).
Loading: code splitting (route > component), tree shaking (needs ES modules), preload vs prefetch vs preconnect, responsive + modern-format images.
Runtime: batch DOM reads/writes (avoid layout thrashing), animate transform/opacity on the compositor, requestAnimationFrame, web workers, virtualization.
Network: immutable hashed assets + long cache, CDN, brotli/gzip, bundle size reduction.
Debugging: measure first (Lighthouse, Performance panel, Network waterfall), diagnose per metric, fix the biggest bottleneck, re-measure.
Goal: Build a complete, interview-ready performance mental model: Core Web Vitals (including INP replacing FID in 2024), loading strategies, runtime optimization, network caching, and a structured "how to debug a slow page" answer.
Continue learning with these related challenges
Understanding frontend performance optimization from the ground up - learn how browsers work and optimize your code accordingly.
A practical guide to crawling, indexing, meta tags, semantics, and Core Web Vitals so your pages rank and stay fast.
Learn how I cut CSS bundle size by 85% and improved page load times by 50% using dynamic imports and automatic code splitting in Next.js—a simple pattern with massive performance impact.
Understanding frontend performance optimization from the ground up - learn how browsers work and optimize your code accordingly.
A practical guide to crawling, indexing, meta tags, semantics, and Core Web Vitals so your pages rank and stay fast.
Learn how I cut CSS bundle size by 85% and improved page load times by 50% using dynamic imports and automatic code splitting in Next.js—a simple pattern with massive performance impact.