Lesson 4 of 8 · 55 min

Rendering, performance, Core Web Vitals

Own LCP, INP, and CLS with field-first diagnosis, the rendering menu (CSR/SSR/SSG/ISR/RSC/PPR), bundle and image strategy, RUM budgets, and interview-ready decision drills.

Lab green, users red

Core Web Vitals are measured on real users at the 75th percentile, not only Lighthouse on your laptop. Senior FE owns LCP, INP, and CLS as product constraints — diagnosed with traces, fixed with the right rendering and asset strategy, guarded with budgets.
As of March 2024, INP replaced FID as the interactivity Core Web Vital. Thresholds to recite: LCP good ≤ 2.5s, INP good ≤ 200ms, CLS good ≤ 0.1 (75th percentile). Poor bands matter for SEO and for “feels janky” product reviews. TTFB, FCP, and TBT are diagnostic metrics, not CWV — but TTFB often eats the LCP budget.

CWV pipeline (say it on the whiteboard)

text
1DNS → TCP → TLS → TTFB → FCP → LCP → (interactions over lifetime) → INP2                              │                        ▲3                         hydration / JS              setState / paint45Field (CrUX / RUM p75) decides product risk.6Lab (Lighthouse, WebPageTest) decides which fix to try next.

LCP — largest contentful paint

LCP is when the largest content element becomes visible. Common LCP elements: hero image, headline block, large text. Diagnosis: Performance panel / web-vitals attribution → name the element → fix discovery, priority, and bytes. Always pair TTFB with LCP: a slow origin makes every image optimization look worse than it is.
  1. 01Discover late — image URL only in CSS/JS; not in initial HTML. Fix: real img or next/image in server HTML.
  2. 02Priority low — no fetchpriority=high / priority prop on hero. Fix: prioritize LCP image only.
  3. 03Bytes heavy — multi-MB hero. Fix: AVIF/WebP, right size, CDN, compression.
  4. 04Blocked render — fonts/CSS long path. Fix: font-display, subset, critical CSS discipline.
  5. 05Server slow — TTFB dominates. Fix: cache, edge, lighter RSC payload, not only client tweaks.
tsx
1import Image from 'next/image'234// Always reserve space (width/height or aspect-ratio) — LCP fix that also protects CLS

INP — interaction to next paint

INP measures responsiveness across the page lifetime: input delay + processing + presentation delay until next paint. Killers: long tasks on main thread, huge re-renders on each keystroke, synchronous JSON parse of big payloads, layout thrash in handlers. A page can load fine (good LCP) and still fail INP on the search box six minutes later.
  1. 01Break long tasks (scheduler.yield / postTask / smaller components).
  2. 02useTransition / debouncing for expensive filters.
  3. 03Virtualize large DOM; avoid rendering 10k nodes “just in case.”
  4. 04Move pure compute off critical path; web workers for heavy CPU when justified.
  5. 05Avoid layout reads/writes interleaving in scroll handlers — batch reads then writes.
  6. 06Third-party scripts often own your INP — defer, facade, or remove.

CLS — cumulative layout shift

CLS sums unexpected layout shifts. Classic causes: images without dimensions, ads/embeds, late web fonts (FOIT/FOUT), injecting banners above existing content, infinite scroll inserting without reserved space. App shells fail CLS too — late auth banners and permission dialogs are product bugs, not marketing-only issues.
css
1/* Reserve space */2img, video { max-width: 100%; height: auto; }3.hero { aspect-ratio: 16 / 9; background: #eee; }4/* Fonts: next/font or font-display + metrics adjust */5/* Prefer transforming skeleton → content in the same box */6/* Avoid inserting sticky banners that push content mid-read */

The rendering menu (decision table)

  1. 01SSG — static docs/marketing; redeploy or rebuild to update; best CDN.
  2. 02ISR — news, blogs, product catalog; revalidate / revalidateTag.
  3. 03SSR — per-user, auth-gated HTML; TTFB and serverless spin cost.
  4. 04PPR — mostly static shell + dynamic holes; modern App Router default mental model.
  5. 05CSR — behind login, no SEO need; largest TTI risk.
  6. 06RSC — static + dynamic islands; less hydration for server-only subtrees.
Interview drill: “PDP for 2M products, SEO critical, price changes hourly.” Answer shape: static shells or tag-based revalidation; on-demand revalidate on price pipeline; client islands for add-to-cart; prioritize LCP image; do not CSR the whole PDP.
tsx
1// PPR-style shell (conceptual)2// next.config: experimental.ppr3export default function ShopPage() {4	return (5		<>6			<h1>Shop</h1> {/* static shell */}7			}>8				 {/* streamed dynamic hole */}9			10		11	)12}

Bundle hygiene

JavaScript is the tax on INP and often LCP (if it blocks). Route-level code splitting is default in Next; still audit heavy client imports (charts, markdown, PDF, moment vs dayjs, icon packs). Client components ship JS even if first rendered on the server — “client” means “hydrates,” not “CSR only.”
tsx
1import dynamic from 'next/dynamic'23const Chart = dynamic(() => import('./RevenueChart'), {4	ssr: false, // only if truly browser-only5	loading: () => ,6})7// Tree-shaking antipatterns:8// import _ from 'lodash' → prefer lodash-es or per-method9// barrel files that re-export everything can poison shaking10// CI: fail PR if critical route client bundle grows > threshold

Images, fonts, third parties

  1. 01Responsive srcset/sizes; do not ship 4000px into a 360px slot.
  2. 02CDN + modern formats; lazy below-fold; eager/priority only LCP.
  3. 03Fonts: subset, few weights, next/font or self-host; avoid layout-shifting fallbacks.
  4. 04Third parties (analytics, chat tags): defer, facade pattern, privacy-aware; they often own your INP.

Diagnosis workflow (say this in interviews)

text
11. Field data first: CrUX / RUM (web-vitals → your backend)22. Lab: Lighthouse mobile + throttling; Chrome Performance + Insights33. Name the metric failing + the element/interaction44. Pick the cheapest fix that hits the causal path55. Re-measure lab AND watch RUM after release (flags)66. Budget: fail CI if bundle or Lighthouse regression exceeds threshold78import { onLCP, onINP, onCLS } from 'web-vitals'9onLCP(send); onINP(send); onCLS(send)

RSC boundary cost

RSC reduces client JS but is not free: server time, serialization size, and waterfalls (await in series) hurt TTFB/LCP. Parallelize fetches; stream with Suspense; do not block the whole page on the slowest panel. Client components still needed for interaction — balance islands vs server. A common senior mistake is serial awaits in a layout that blocks every child.

RUM budgets as product constraints

Write budgets like SLOs: “Authenticated dashboard p75 INP ≤ 200ms on mid mobile; marketing LCP ≤ 2.5s.” Attach alerts and feature-flag rollbacks. Performance without a budget is a hobby; performance with a budget is ownership.
Exploring React Performance — React ConfReact team / conference

Interview answer bank — performance

  1. 01Q: Lab vs field — which do you trust for ship decisions? Field RUM / CrUX at p75 is the product truth for CWV. Lab (Lighthouse, WebPageTest) is for diagnosis and regression gates. Shipping only on lab 100 while field p75 is poor is a classic mid-level trap. Use lab to pick the fix; use field to declare victory.
  2. 02Q: How does TTFB relate to LCP? TTFB is on the critical path before any contentful paint. A 1.5s TTFB leaves only 1.0s of budget for a “good” 2.5s LCP. Fix origin cache, edge, lighter RSC payloads, and waterfalls before micro-optimizing a 20kb hero that is already correctly prioritized.
  3. 03Q: What is a long task and why does it hurt INP? A main-thread task over ~50ms blocks input handling and paint. INP includes input delay while the main thread is busy. Break work with yielding, defer non-urgent React updates with startTransition, virtualize DOM, and move heavy parse/compute off the critical path.
  4. 04Q: When is next/dynamic with ssr:false justified? Only for truly browser-only modules (WebGL, certain chart libs, window-only SDKs). Using ssr:false for ordinary UI throws away SSR HTML for that island and can hurt LCP/CLS if the island is above the fold. Prefer RSC + small client islands first.
  5. 05Q: How do third parties destroy INP? Analytics, chat widgets, and A/B tags often run long tasks on the main thread and compete with your interaction handlers. Defer them, load behind interaction (facade), or remove. In interviews, naming third parties as an INP suspect is strong senior signal.
Partial Prerendering is the modern default mental model for App Router pages that are mostly static with dynamic holes: ship the shell from cache, stream personalized panels behind Suspense. It combines static speed with request-time dynamism without forcing the entire route into pure SSR or pure CSR.
text
1// Performance budget sketch (CI + RUM)2// Critical route client JS: ≤ 180kb gzipped (example — set per product)3// LCP p75 marketing: ≤ 2.5s4// INP p75 app shell: ≤ 200ms5// CLS p75: ≤ 0.16// Alert if weekly p75 regresses >10% after release; attach flag rollback owner78// Diagnosis one-liner for interviews:9// “Field says INP bad on search; lab Performance panel shows 180ms long task10// in filter reducer; fix with deferred value + virtualized results; re-check RUM.”
Rendering interview matrix: SEO critical + rarely changing → SSG/ISR. SEO critical + per-user → SSR/RSC with cache where safe. Private tools → CSR or SSR without SEO pressure. Catalog scale with hourly price → ISR + on-demand revalidateTag from the price pipeline. Always name the LCP element in the same breath as the rendering choice.

Field budgets, third parties, and PPR recap

Write performance budgets the way you write SLOs: numeric, owned, and rolled back with flags. Example: marketing LCP p75 ≤ 2.5s; app shell INP p75 ≤ 200ms; critical route client JS ≤ 180kb gzipped. When a release regresses field p75 by more than about 10%, the flag owner rolls back first and debugs second. That is ownership language from L1 applied to CWV.
Third-party scripts are often the real INP villains. Tag managers, chat widgets, session replay, and A/B SDKs schedule long tasks that delay click and key handling. Senior moves: load behind consent and interaction (facade), defer to requestIdleCallback where safe, isolate on a worker when the vendor allows, or delete the tag. In an interview, always ask “what third parties ship on this page?” before micro-optimizing your own React tree.
Partial Prerendering mental model: the static shell (nav, layout chrome, product title skeleton structure) is prerendered and cacheable; dynamic holes (personalized price, cart count, reviews) stream through Suspense. Users see structure immediately; slow panels trail without blocking the whole HTML document. Pair with reserved skeleton heights so streaming does not create CLS. Cite nextjs.org partial prerendering docs when you name PPR.
  1. 01LCP checklist — name element; early discover in HTML; priority; right bytes; fix TTFB if origin-bound.
  2. 02INP checklist — profile interaction; break long tasks; transition/deferred; virtualize; tame third parties.
  3. 03CLS checklist — dimensions on media; font strategy; no surprise inserts; skeleton → content same box.
  4. 04Bundle checklist — route split; dynamic heavy widgets; no poison barrels; CI size gate.
  5. 05RUM checklist — web-vitals to backend; dashboards by route; alert + flag owner.
text
1// Interview 60s answer: PDP rendering2// SEO critical + 2M SKUs + hourly price:3// - SSG/ISR shell or PPR static product chrome4// - revalidateTag('product:'+id) from price pipeline5// - next/image priority on hero with sizes6// - client island AddToCart only7// - do NOT CSR the entire PDP8// - measure field LCP on mobile after ship
docsweb.dev — Interaction to Next Paint (INP)web.devdocsweb.dev — Optimize LCPweb.devdocsweb.dev — Optimize CLSweb.devdocsweb.dev — Learn Core Web Vitalsweb.devdocsNext.js — Image Optimizationnextjs.orgdocsNext.js — Partial Prerenderingnextjs.org

Checkpoint

RUM shows p75 LCP 3.8s. Attribution: a hero image discovered only after a client fetch. Best fix?

AAdd a service worker to cache the SPA shell.BRender the hero in server HTML with correct dimensions and priority; serve modern formats at the right size.CIncrease the Lighthouse CPU throttle to match reality.
Sign up free to answer and see why

Checkpoint

Search keystrokes feel laggy; INP poor on mid phones. First investigation?

AReplace React with vanilla DOM for the input only — rewrite the app.BProfile the interaction: long tasks / re-render fanout; defer heavy filtering (transition/deferred) and virtualize results.CDisable CSS animations globally.
Sign up free to answer and see why

Checkpoint

CLS spikes when infinite scroll appends images. What design prevents it?

ASet animation-duration to 0 for all images.BKnown aspect ratios / width-height, skeletons with reserved space, avoid inserting above the viewport without anchoring.CLoad all images eagerly up front.
Sign up free to answer and see why

Checkpoint

Pick rendering for a docs site updated a few times per day, SEO critical.

AFully CSR SPA.BSSG or ISR with revalidation on publish — CDN-cached HTML, refresh when content changes.CSSR on every request with no cache.
Sign up free to answer and see why

Checkpoint

Which statement about Core Web Vitals is correct?

AGood thresholds are judged at the median of lab runs only.BINP replaced FID; evaluate p75 real-user interaction latency, not just first input.CCLS only counts shifts in the first second after load.
Sign up free to answer and see why

Can you diagnose LCP/INP/CLS from a trace and pick a rendering mode for a product constraint?

New to itGetting thereConfident

Takeaways

  • CWV = LCP/INP/CLS at RUM p75 — lab is a tool, field is truth.
  • Name the LCP element; fix discovery, priority, bytes, TTFB.
  • INP is lifetime interactivity — kill long tasks and interaction re-render fanout.
  • CLS dies with reserved space, careful injection, and font strategy.
  • Rendering menu is a product decision; bundles and third parties are INP taxes.

Next: accessibility and design-system stewardship — architecture, not a late checklist.

Sources

Free to read · better with Enzo

Learn it with Enzo

Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.