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 type | Blocks parse | Blocks render |
|---|---|---|
Sync <script> | Yes | Often (indirectly) |
CSS (<link>) | No | Yes |
defer / async JS | No | No (until execute) |
| Fonts | No | Can delay text paint (FOIT) |
| Images (no dims) | No | Can 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 constructionThe 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:
- Inline critical CSS in
<head>(above-fold rules only) - Load full stylesheet async —
media="print" onload="this.media='all'"orrel="preload" as="style" - Split by route — don’t ship entire app CSS on landing page
JS fixes:
deferortype="module"for app codeasyncfor independent scripts (analytics)- Move scripts to end of
<body>if defer unavailable - 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:
- Identify above-fold viewport (mobile-first — 375×667)
- Extract used rules (Critical, Penthouse, or build plugin)
- Inline in
<head>, async-load full stylesheet - 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 linkMaintenance 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:
| Phase | Budget | Action |
|---|---|---|
| TTFB | < 600ms | Edge cache, SSG, CDN |
| HTML download | < 200ms | Compression, small shell |
| Critical CSS | inline < 14KB | Extract, purge |
| Critical JS | 0 blocking | defer, code-split |
| LCP resource | < 800ms | preload, WebP, SSR in HTML |
| Render | < 200ms | No 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 onlyValidate: 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
@importchains in CSS delaying CSSOM