Topics
v6.0.0
On this page · sections

Critical Rendering Path

The critical rendering path is the minimum sequence of steps from first byte to first meaningful paint — optimizing it means minimizing render-blocking resources and main-thread work before LCP.

Resource typeBlocks parseBlocks render
Sync <script>YesOften (indirectly)
CSS (<link>)NoYes
defer / async JSNoNo (until execute)
FontsNoCan delay text paint (FOIT)
Images (no dims)NoCan cause CLS

CRP Stages and Blockers

Definition

HTML bytes → DOM CSS bytes → CSSOM (blocks first render) DOM + CSSOM → Render tree → Layout → Paint → Composite Sync JS → blocks DOM construction

The critical path is the longest dependent chain to first paint — not every resource, only what’s needed for above-fold content.

<!-- Render-blocking --> <link rel="stylesheet" href="/styles.css"> <!-- Parser-blocking --> <script src="/legacy.js"></script> <!-- Non-blocking --> <link rel="stylesheet" href="/below-fold.css" media="print" onload="this.media='all'"> <script defer src="/app.js"></script> <script type="module" src="/app.mjs"></script>

Minimize critical path depth (serial requests) and width (bytes to download + parse).

Interview: CSS blocks render. Sync JS blocks parse. Critical path = minimum chain to first paint.

Eliminate Render-Blocking CSS and JS

Definition

CSS fixes:

  1. Inline critical CSS in <head> (above-fold rules only)
  2. Load full stylesheet async — media="print" onload="this.media='all'" or rel="preload" as="style"
  3. Split by route — don’t ship entire app CSS on landing page

JS fixes:

  1. defer or type="module" for app code
  2. async for independent scripts (analytics)
  3. Move scripts to end of <body> if defer unavailable
  4. Code-split — route JS not in initial download
<head> <style> /* Critical — hero, nav, typography (~14KB max) */ .hero { min-height: 60vh; } .nav { display: flex; } </style> <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="/styles.css"></noscript> <script defer src="/app.js"></script> </head>

Remove unused CSS — PurgeCSS/Tailwind JIT. @import in CSS is serial — prefer <link> tags for parallel fetch.

Interview: Inline critical CSS, async the rest. defer/module for app JS. No sync scripts in head.

Critical CSS Extraction and Maintenance

Definition

Critical CSS = minimum rules to render above-fold content without FOUC.

Workflow:

  1. Identify above-fold viewport (mobile-first — 375×667)
  2. Extract used rules (Critical, Penthouse, or build plugin)
  3. Inline in <head>, async-load full stylesheet
  4. CI diff — fail if critical CSS exceeds budget (~14KB)
// Build-time plugin concept (Vite/webpack critical-css plugin) // 1. Render route HTML // 2. Extract computed styles for above-fold selectors // 3. Emit inline <style> block + deferred full CSS link

Maintenance pitfalls:

  • Critical CSS drifts as UI changes — automate extraction in CI
  • Duplicated rules between inline and full sheet — acceptable for speed
  • Dark mode / media queries — include in critical if above-fold depends on them

Tailwind: @tailwind base/components/utilities — purge unused; critical subset is harder — consider per-route CSS with CSS modules or scoped styles.

Interview: Automate critical CSS extraction in CI. Manual inline drifts quickly. Budget ~14KB.

Fonts and LCP Image

Definition

Fonts: FOIT (invisible text) or FOUT (fallback flash) both hurt perceived perf and CLS.

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> <style> @font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: swap; } </style>

Use next/font or self-host — eliminates runtime Google Fonts chain. Match fallback metrics with size-adjust, ascent-override.

LCP image: must be discoverable in first HTML chunk.

<img src="/hero-800.webp" srcset="/hero-800.webp 800w, /hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 1200px" width="1200" height="630" fetchpriority="high" alt="Hero"> <link rel="preload" as="image" href="/hero-800.webp" imagesrcset="/hero-800.webp 800w, /hero-1200.webp 1200w" imagesizes="(max-width: 768px) 100vw, 1200px">

Never lazy-load LCP image. Explicit width/height prevents CLS.

Interview: Preload LCP image with fetchpriority high. font-display swap + metric-matched fallback. No lazy on hero.

Sub-2s LCP on 3G Playbook

Definition

Target: LCP < 2.5s on Slow 4G / 3G (Lighthouse throttling).

Phase budget:

PhaseBudgetAction
TTFB< 600msEdge cache, SSG, CDN
HTML download< 200msCompression, small shell
Critical CSSinline < 14KBExtract, purge
Critical JS0 blockingdefer, code-split
LCP resource< 800mspreload, WebP, SSR in HTML
Render< 200msNo sync JS, minimal hydration
SSG/SSR landing HTML → inline critical CSS → preconnect API + CDN → preload LCP image (fetchpriority=high) → defer all JS → hero img in HTML (not client fetch) → no third-party in head → service worker for repeat visits only

Validate: Lighthouse mobile + web-vitals field RUM. Synthetic 3G test in CI on every deploy to production branch.

Third-party audit: defer analytics to after load or requestIdleCallback. Facade pattern for embeds (click-to-load).

Interview: Hero in HTML, critical CSS inline, zero render-blocking JS, preload LCP image. Attack largest timing bucket first.

Senior Interview Summary

Mental model

Critical path = minimum dependent chain to first paint Minimize: blocking CSS bytes, sync JS, serial request depth Maximize: early discovery (preload), parallel fetch (H2)

Practical rules

  • Inline critical CSS; async full stylesheet
  • defer/module for all app JS — zero sync scripts in head
  • LCP image in SSR HTML with dimensions + preload
  • Self-host fonts with font-display swap
  • Perf budget per route — CI gate on Lighthouse mobile

What to say in interviews

“I’d map the critical rendering path: inline critical CSS for above-fold, defer all JavaScript, put the LCP hero image in server HTML with width/height and preload it with fetchpriority high. Full stylesheet loads async. I’d automate critical CSS extraction in CI with a 14KB budget and validate with Lighthouse mobile throttling plus field LCP breakdown.”

Common traps

  • Entire app CSS in render-blocking link
  • Sync analytics in head
  • Client-side hero fetch — LCP waits on JS
  • Lazy-loading the LCP image
  • @import chains in CSS delaying CSSOM

Last updated on