Understanding how browsers render web pages is essential for performance optimization. This guide explains the Critical Rendering Path (CRP), rendering stages, performance metrics, and practical optimization strategies.
The Critical Rendering Path is the sequence of steps a browser takes to:
The CRP directly impacts First Contentful Paint (FCP) and Largest Contentful Paint (LCP), which are key Web Vitals metrics.
Why it matters: Any resource that blocks the CRP delays when your page becomes visible and interactive.
Render-blocking means the browser pauses or delays rendering while waiting for a resource to be fetched and processed.
| Resource | Blocks Rendering | Why | Impact |
|---|---|---|---|
| CSS | โ Yes | Needed for styles before painting | Highest priority |
| JavaScript (default) | โ Yes | Can modify DOM/CSSOM/styles | Must be optimized |
| JavaScript (defer) | โ No* | Deferred until DOM ready | Ideal for most |
| JavaScript (async) | โ No* | Runs independently | Best for non-critical |
| Fonts | โ ๏ธ Partial | Text may be invisible (FOIT) | Use font-display: swap |
| Images | โ No | Loaded separately | Lazy load below fold |
*May execute before rendering completes, but doesn't intentionally block it.
Without Optimization:
[CSS Download] -> [Parse CSS] -> [Render] = Delayed FCP
With defer/async JS:
[JS Download] -> [HTML Parse] -> [CSS] -> [Render] = Early FCP + JS runs later
With Critical CSS:
[Critical CSS inline] -> [Render quickly] + [Non-critical async] = Optimized FCP
Browser -> DNS Lookup -> TCP Handshake -> HTTP Request -> Server Response
Key Points:
Optimization:
// Use DNS prefetch for external domains <link rel="dns-prefetch" href="//cdn.example.com"> // Preconnect for critical resources <link rel="preconnect" href="//fonts.googleapis.com"> // Preload critical resources <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
HTML Bytes -> Tokenization -> Tree Construction -> DOM Tree
Process:
<tag>, text, attributes)Example:
<html> <head> <title>Page</title> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>Hello</h1> <p>Content</p> </body> </html>
Results in DOM tree:
Document
+-- html
| +-- head
| | +-- title: "Page"
| | +-- link (stylesheet reference)
| +-- body
| +-- h1: "Hello"
| +-- p: "Content"
Key Point: Parsing is streamlined - visible content can render before entire HTML is parsed.
CSS Bytes -> Tokenization -> Parse Rules -> CSSOM Tree
Process:
<link> or <style> tags triggers downloadExample:
/* In styles.css */ body { font-size: 16px; } h1 { color: blue; font-size: 32px; } p { color: gray; }
Results in CSSOM tree:
StyleSheetList
+-- body
| +-- font-size: 16px
| +-- margin: 0 (inherited default)
| +-- padding: 0 (inherited default)
+-- h1
| +-- color: blue
| +-- font-size: 32px
| +-- (inherits font-size override)
+-- p
+-- color: gray
+-- (inherits font-size from body)
Critical Detail: CSS is render-blocking. The browser waits for CSS before rendering because it needs all styles to avoid re-rendering.
// CSS render-blocking flow: // 1. <link rel="stylesheet"> tag encountered // 2. CSS file requested // 3. HTML parsing continues (non-blocking download) // 4. BUT: Rendering blocked until CSS parsed // 5. Why? To prevent FOUC (Flash of Unstyled Content)
<!-- CSS blocks rendering to prevent FOUC (Flash of Unstyled Content) --> <!-- WITHOUT CSS blocking: --> <!-- User sees unstyled HTML (terrible UX) --> <!-- Then CSS loads and page "snaps" into place --> <!-- WITH CSS blocking (default browser behavior): --> <!-- Browser waits for CSS before rendering --> <!-- Page appears styled correctly on first render -->
Timeline comparison:
โ WITHOUT CSS blocking:
[HTML Parse] -> [Render unstyled] -> [CSS arrives] -> [Paint with styles]
^ FOUC (Flash) - bad experience
โ
WITH CSS blocking (current approach):
[HTML Parse] -> [Wait for CSS] -> [CSS arrives] -> [Render with styles]
Mitigation Strategies:
<!-- โ Inline critical CSS (no wait) --> <style> /* Only styles needed above-the-fold */ body { margin: 0; font-family: Arial; } header { background: #333; } h1 { color: white; } </style> <!-- โ Load non-critical CSS with media queries --> <link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)"> <!-- โ Preload critical resources --> <link rel="preload" href="critical.css" as="style">
JavaScript can significantly impact the rendering pipeline. This is one of the most critical optimizations.
<!-- HTML parsing STOPS until JS is fetched & executed --> <!-- Rendering also blocked because DOM may not be ready --> <script src="app.js"></script>
Flow:
Parsing -> <script> tag -> [STOP PARSING]
v
Download app.js
v
Execute app.js (can modify DOM/styles)
v
Resume parsing
Impact: Every second of JS execution delays First Contentful Paint.
defer Attribute: NOT RENDER-BLOCKING<!-- HTML parsing continues; JS runs after parsing completes --> <!-- Rendering happens as planned, then JS runs --> <script src="app.js" defer></script>
Flow:
[Parsing continues] -> [CSS done] -> [Render happens] -> [defer scripts run]
v
DOMContentLoaded fired
Order guaranteed: Multiple defer scripts run in order.
When to use: For application framework (React, Vue, etc.), main app code.
async Attribute: NOT RENDER-BLOCKING PARSING, but may interrupt rendering<!-- HTML parsing continues; JS runs as soon as downloaded --> <!-- May execute before rendering if downloaded quickly --> <script src="analytics.js" async></script>
Flow:
[Parsing continues] -> Downloaded -> [STOP & Execute] -> [Resume rendering]
Order NOT guaranteed: Multiple async scripts run independently.
When to use: Analytics, third-party ads, non-critical tracking.
type="module" (ES6 Modules)<!-- Treated like defer by default (deferred execution) --> <!-- Maintains order with other modules --> <script type="module" src="app.js"></script>
// โ BLOCKS RENDERING (default script tag) // Page invisible while heavy-calculation.js is downloaded & executed <script src="heavy-calculation.js"></script> // Impact: FCP delayed by entire JS execution time // โ BETTER: Defer execution <script src="heavy-calculation.js" defer></script> // Impact: HTML parsed + rendering starts, THEN JS runs // FCP appears much sooner // โ BEST: For non-critical analytics/tracking <script src="analytics.js" async></script> // Impact: Minimal impact on FCP // Analytics runs whenever it finishes downloading // โ MODERN: Use Web Workers for heavy computation <script> const worker = new Worker('expensive-task.js'); // Main thread stays free for rendering worker.postMessage(data); worker.onmessage = (e) => { // Update UI with results, doesn't block rendering updateUI(e.data); }; </script> // โ BEST PRACTICE: Inline critical bootstrap only <script> // Minimal code: just app initialization window.config = { version: '1.0' }; // Heavy app logic loads via defer/async </script> <script src="app.js" defer></script>
<!-- โ Multiple blocking scripts (each one blocks rendering) --> <script src="lib1.js"></script> <script src="lib2.js"></script> <script src="app.js"></script> <!-- FCP delayed by: lib1 + lib2 + app execution time --> <!-- โ Use defer for all non-critical scripts --> <script src="lib1.js" defer></script> <script src="lib2.js" defer></script> <script src="app.js" defer></script> <!-- FCP appears while these download in parallel --> <!-- โ Large inline scripts (parse + execute blocks rendering) --> <script> // 100KB of JavaScript here... page frozen const heavyComputation = () => { /* ... */ }; heavyComputation(); </script> <!-- โ Keep inline scripts minimal, move rest to external files --> <script> // Only initialization code (< 1KB) window.appConfig = { /* ... */ }; </script> <script src="app.js" defer></script>
DOM Tree + CSSOM Tree -> Render Tree
Process:
display: none (completely hidden)<head> and <title> tagsvisibility: hidden (reserved in layout)opacity: 0 (still part of render tree)position: absolute (still rendered)Example:
<style> .hidden { display: none; } .invisible { visibility: hidden; } </style> <div>Visible</div> <div class="hidden">Not in render tree</div> <div class="invisible">In render tree, but hidden</div>
Resulting Render Tree:
Render Tree
+-- div: "Visible"
+-- div: (invisible, space reserved)
// .hidden div is NOT in render tree
Key Point: Each render tree node = box with CSS computed styles.
Render Tree -> Calculate Positions & Sizes -> Layout Boxes
Process:
Example:
// Triggers layout calculation (Reflow) const height = element.offsetHeight; const width = element.clientWidth; const rect = element.getBoundingClientRect(); element.style.width = '100px'; // Layout recalculation // Why? Browser must calculate actual dimensions console.log(height); // Can't just guess, must measure
Common Reflow-Triggering Operations:
// Reading layout properties (forces layout calculation) element.offsetWidth // Triggers reflow element.offsetHeight // Triggers reflow element.getBoundingClientRect() // Triggers reflow element.scrollHeight // Triggers reflow window.getComputedStyle(el) // Triggers reflow (sometimes) // Modifying layout properties (triggers reflow) element.style.width = '100px' // Reflow element.classList.add('resize') // Reflow (if CSS changes layout) element.innerText = 'New Text' // Reflow (if height changes)
Optimization Example:
// โ BAD: Multiple reflows for (let i = 0; i < 100; i++) { element.style.width = (i * 10) + 'px'; // Reflow each iteration console.log(element.offsetWidth); // Reflow each iteration } // โ GOOD: Batch reflows element.style.transition = 'width 1s'; element.style.width = '1000px'; // Single reflow + animation // โ BETTER: DocumentFragment for DOM additions const frag = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const div = document.createElement('div'); div.textContent = i; frag.appendChild(div); // No reflow yet } container.appendChild(frag); // Single reflow // โ BEST: Use requestAnimationFrame for reads/writes function optimizedUpdate() { requestAnimationFrame(() => { // All reads const heights = Array.from(elements).map(el => el.offsetHeight); requestAnimationFrame(() => { // All writes elements.forEach((el, i) => el.style.height = heights[i] + 'px'); }); }); }
Layout Boxes -> Rasterization -> Paint Records -> Display List
Process:
Example:
// These operations trigger painting (but not reflow) element.style.backgroundColor = 'red'; // Paint element.style.color = 'blue'; // Paint element.style.opacity = 0.5; // Paint element.style.boxShadow = '0 0 10px black'; // Paint // BUT: These CSS properties are paint-efficient // (handled by GPU compositing, not CPU painting) element.style.transform = 'translate(10px, 10px)'; // Composite only element.style.opacity = 0.5; // Composite only (with will-change)
Paint Performance Layers:
Expensive (triggers paint):
- background-color changes
- border changes
- text-shadow changes
Less Expensive (composite-only):
- transform changes
- opacity changes (with GPU acceleration)
- filter changes (modern browsers optimize)
Paint Records + Layers -> Composite -> GPU Rendering -> Display
Process:
Optimize for Compositing:
// โ EFFICIENT: Transform (composite-only, GPU accelerated) element.style.transform = 'translate(100px, 100px)'; // โ EFFICIENT: Opacity (composite-only with GPU) element.style.opacity = 0.5; // โ EFFICIENT: Filter (GPU accelerated in modern browsers) element.style.filter = 'blur(5px)'; // โ EXPENSIVE: Left/top (triggers layout + paint) element.style.left = '100px'; element.style.top = '100px'; // โ TIP: Force GPU compositing with will-change element.style.willChange = 'transform, opacity';
1. Parse HTML
+- Stream tokenization
+- Create DOM nodes
+- Encounter <link> (CSS) and <script> (JS)
2. Fetch & Parse CSS
+- Download stylesheet
+- Parse selectors, rules, cascade
+- Build CSSOM
+- โ ๏ธ Blocks rendering until complete
3. Fetch & Execute JavaScript (depends on attributes)
+- Default: Blocks HTML parsing
+- defer: Executes after parsing
+- async: Executes when ready
4. Construct Render Tree
+- Combine DOM + CSSOM
+- Remove display:none elements
+- Include computed styles
5. Layout (Reflow)
+- Calculate positions & sizes
+- Build box model
+- Compute viewport constraints
6. Paint
+- Rasterize layout into pixels
+- Create paint records
+- Build display list
7. Composite
+- Blend layers
+- GPU acceleration
+- Final display bitmap
8. Display on Screen
+- First Contentful Paint (FCP)
+- Largest Contentful Paint (LCP)
The JavaScript event loop is intrinsically connected to the browser's rendering cycle. Understanding this relationship is crucial for performance optimization.
+---------------------------------------------------------+
| Browser Event Loop & Render Cycle |
+---------------------------------------------------------+
1. Execute JavaScript
+- Call Stack processes synchronous code
+- setTimeout, Promises, event listeners queued
2. Execute Microtasks (โก High Priority)
+- Promise.then()
+- Promise.catch()
+- Promise.finally()
+- queueMicrotask()
+- MutationObserver callbacks
+- Process all until queue empty
3. Check for Rendering Opportunity
+- Is there visual update needed?
+- If YES: Proceed to rendering
+- If NO: Skip to next task
4. Rendering Phase (if needed)
+- recalcStyle() - Apply CSS
+- layout() - Reflow
+- paint() - Rasterize
+- composite() - GPU blend
5. Execute Macrotasks (Lower Priority)
+- setTimeout callbacks
+- setInterval callbacks
+- I/O operations
+- UI events (click, scroll)
+- requestAnimationFrame (actually executes BEFORE rendering)
6. Back to Step 1
console.log('1: Script start'); // Synchronous (main thread) // Macrotask: setTimeout setTimeout(() => { console.log('6: Macrotask (setTimeout)'); }, 0); // Microtask: Promise Promise.resolve() .then(() => { console.log('3: Microtask (Promise.then)'); }) .then(() => { console.log('4: Microtask (Promise.then 2)'); }); // Macrotask alternative: setImmediate (Node.js only) // Microtask: queueMicrotask queueMicrotask(() => { console.log('5: Microtask (queueMicrotask)'); }); console.log('2: Script end'); // Synchronous (main thread) /* Output Order: 1: Script start 2: Script end 3: Microtask (Promise.then) 4: Microtask (Promise.then 2) 5: Microtask (queueMicrotask) 6: Macrotask (setTimeout) */
requestAnimationFrame (RAF) is not a microtask or macrotaskโit's scheduled during the rendering phase:
console.log('1: Start'); setTimeout(() => console.log('4: setTimeout'), 0); requestAnimationFrame(() => console.log('3: RAF')); Promise.resolve().then(() => console.log('2: Promise')); /* Output Order: 1: Start 2: Promise (microtask) 3: RAF (scheduled before rendering) 4: setTimeout (macrotask) */
Why RAF timing matters:
// โ BAD: DOM changes with setTimeout (batches with next frame) setTimeout(() => { element.style.transform = 'translateX(100px)'; // Browser may have already rendered, causing extra work }, 0); // โ GOOD: DOM changes with RAF (synchronized with rendering) requestAnimationFrame(() => { element.style.transform = 'translateX(100px)'; // Guaranteed to be before rendering }); // โ BEST: Use RAF for animation loop let x = 0; function animate() { x += 5; element.style.transform = `translateX(${x}px)`; if (x < 500) { requestAnimationFrame(animate); } } animate();
The browser checks for rendering work between every macrotask:
// Task 1: Macrotask setTimeout(() => { console.log('Task 1'); element.style.backgroundColor = 'red'; // After this macrotask -> Browser checks for rendering // -> Rendering happens (if needed) }, 0); // Task 2: Macrotask setTimeout(() => { console.log('Task 2'); element.style.backgroundColor = 'blue'; // After this macrotask -> Browser checks for rendering again // -> Rendering happens (if needed) }, 0); /* Timeline: 1. Execute Task 1 2. Check rendering -> Render with red background 3. Execute Task 2 4. Check rendering -> Render with blue background */
All microtasks must complete before rendering can occur:
// Heavy computation in microtask Promise.resolve() .then(() => { // This runs BEFORE rendering let sum = 0; for (let i = 0; i < 1000000000; i++) { sum += i; // Long computation } // Rendering is BLOCKED until this completes }); element.style.backgroundColor = 'red'; // Red background delayed by microtask computation
Optimization: Use macrotasks for heavy work to allow rendering:
// โ BETTER: Break work into macrotasks function heavyComputation() { // Do work in chunks, yield to rendering setTimeout(() => { // Process chunk 1 processChunk(0, 100); if (hasMoreWork) { heavyComputation(); // Continue after rendering } }, 0); }
// Measure time spent in different phases const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(`${entry.name}: ${entry.duration}ms`); } }); observer.observe({ entryTypes: ['measure', 'navigation'] }); // Mark event loop phases performance.mark('script-start'); // ... heavy JS work ... performance.mark('script-end'); performance.measure('script', 'script-start', 'script-end'); // Rendering performance performance.mark('render-start'); element.style.width = '100px'; performance.mark('render-end'); performance.measure('render', 'render-start', 'render-end');
console.log('=== PHASE 1: Synchronous Code ==='); console.log('1. Start'); setTimeout(() => { console.log('=== PHASE 3: Macrotask (after microtasks) ==='); console.log('5. setTimeout'); }, 0); Promise.resolve() .then(() => { console.log('=== PHASE 2: Microtask ==='); console.log('3. Promise'); return Promise.resolve(); }) .then(() => { console.log('4. Promise chained'); }); queueMicrotask(() => { console.log('2. queueMicrotask (after Promise, same phase)'); }); console.log('End of script'); /* Actual Output: 1. Start End of script 3. Promise 2. queueMicrotask (after Promise, same phase) 4. Promise chained [RENDERING HAPPENS HERE] 5. setTimeout */
| Action | Queue | Blocks Rendering | Timing |
|---|---|---|---|
| Sync code | Main | โ Yes | Immediate |
setTimeout | Macrotask | โ No | After current + render |
Promise.then() | Microtask | โ Yes | Immediate (before render) |
requestAnimationFrame | Animation | N/A | Right before render |
MutationObserver | Microtask | โ Yes | Immediate (before render) |
| Metric | What It Measures | Target |
|---|---|---|
| FCP (First Contentful Paint) | First pixel painted | < 1.8s |
| LCP (Largest Contentful Paint) | Largest element visible | < 2.5s |
| CLS (Cumulative Layout Shift) | Unexpected layout shifts | < 0.1 |
| FID (First Input Delay) | Time to respond to input | < 100ms |
| TTFB (Time to First Byte) | Server response time | < 600ms |
// DOMContentLoaded: DOM ready, before all resources document.addEventListener('DOMContentLoaded', () => { console.log('DOM ready, rendering complete, but images may still load'); }); // load: All resources loaded (images, CSS, etc.) window.addEventListener('load', () => { console.log('Page fully loaded, all resources fetched'); }); // Performance API for custom measurements const perfData = performance.getEntriesByType('navigation')[0]; console.log('Time to First Byte:', perfData.responseStart - perfData.requestStart); console.log('DOM Content Loaded:', perfData.domContentLoadedEventEnd - perfData.fetchStart); console.log('Page Load Time:', perfData.loadEventEnd - perfData.fetchStart);
<!-- โ Default: Blocks rendering --> <link rel="stylesheet" href="all-styles.css"> <!-- Page won't render until this CSS is downloaded & parsed --> <!-- โ BEST: Inline critical CSS --> <head> <style> /* Only above-the-fold styles (< 10KB) */ body { margin: 0; font-family: Arial; } header { background: #333; color: white; } .hero { width: 100%; height: auto; } </style> <!-- Defer non-critical CSS --> <link rel="stylesheet" href="non-critical.css" media="print"> </head> <!-- โ Split CSS by media query (non-blocking for other devices) --> <link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)"> <link rel="stylesheet" href="mobile.css" media="(max-width: 1023px)"> <!-- โ Preload critical CSS with higher priority --> <link rel="preload" href="critical.css" as="style"> <link rel="stylesheet" href="critical.css"> <!-- โ Load non-critical CSS asynchronously --> <link rel="preload" href="secondary.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="secondary.css"></noscript>
Impact:
<!-- โ WORST: Multiple blocking scripts (each delays rendering) --> <script src="jquery.min.js"></script> <script src="bootstrap.js"></script> <script src="app.js"></script> <!-- FCP delayed by: jQuery load + parse + exec + Bootstrap load + parse + exec + app load + parse + exec --> <!-- โ GOOD: Single async-able scripts for non-critical --> <script src="analytics.js" async></script> <script src="ads.js" async></script> <!-- โ BETTER: Defer all non-critical scripts --> <script src="jquery.min.js" defer></script> <script src="bootstrap.js" defer></script> <script src="app.js" defer></script> <!-- All load in parallel; rendering not blocked --> <!-- โ BEST: Minimal inline + defer everything else --> <script> // Only essential initialization (< 1KB) window.appConfig = { apiUrl: '/api' }; window.ready = false; document.addEventListener('DOMContentLoaded', () => { window.ready = true; }); </script> <!-- Load frameworks & app code deferred --> <script src="react.min.js" defer></script> <script src="app.js" defer></script> <!-- Non-critical (tracking, ads) as async --> <script src="google-analytics.js" async></script> <script src="ads.js" async></script>
Impact on FCP:
Scenario 1 - Blocking Scripts:
JS Download (2s) + Parse (0.5s) + Execute (0.5s) = 3s before rendering
Scenario 2 - All defer:
[HTML parse] (0.5s) + [JS downloads in parallel] + [Render in 0.2s]
= FCP at 0.7s (then JS executes)
Benefit: 4x faster FCP!
<!-- โ Minimize CSS size --> <!-- Minify to reduce parse time --> <link rel="stylesheet" href="styles.min.css"> <!-- โ Reduce CSS specificity --> /* โ High specificity (slower to parse & apply) */ body > div.container > section > div > p.intro { color: blue; } /* โ Lower specificity (faster) */ .intro-text { color: blue; } <!-- โ Remove unused CSS --> <!-- Use PurgeCSS, UnCSS, or Tailwind to eliminate unused styles -->
<!-- โ DNS Prefetch for external domains --> <link rel="dns-prefetch" href="//cdn.example.com"> <link rel="dns-prefetch" href="//fonts.googleapis.com"> <!-- โ Preconnect for critical servers --> <link rel="preconnect" href="//cdn.example.com"> <link rel="preconnect" href="//fonts.googleapis.com" crossorigin> <!-- โ Prefetch for likely navigation --> <link rel="prefetch" href="//next-page.com"> <!-- โ Preload for critical resources --> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="critical.css" as="style"> <link rel="preload" href="app.js" as="script">
<!-- โ Default: Text invisible until font loads (FOIT) --> <style> body { font-family: 'MyFont'; } </style> <link href="//fonts.googleapis.com/css?family=MyFont" rel="stylesheet"> <!-- โ Font-display swap: Show fallback immediately --> <style> @font-face { font-family: 'MyFont'; src: url('font.woff2') format('woff2'); font-display: swap; /* Critical: avoid invisible text */ } </style> <!-- โ Preload critical fonts --> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> <!-- โ Load fonts with async --> <link rel="preload" href="secondary-font.woff2" as="font" type="font/woff2" crossorigin>
<!-- โ Images don't block rendering, but they do block LCP --> <img src="hero.jpg" alt="Hero"> <!-- โ Lazy load below-the-fold images --> <img src="hero.jpg" alt="Hero" loading="lazy"> <!-- โ Responsive images for different devices --> <img srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 2000w" sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 100vw" src="medium.jpg" alt="Hero" > <!-- โ Use modern formats with fallback --> <picture> <source srcset="image.webp" type="image/webp"> <source srcset="image.jpg" type="image/jpeg"> <img src="image.jpg" alt="Image"> </picture>
// โ Multiple reflows const container = document.getElementById('container'); container.style.width = '100px'; container.style.height = '100px'; container.style.margin = '10px'; // โ Single reflow (batch changes) container.style.cssText = 'width: 100px; height: 100px; margin: 10px;'; // OR use classList container.classList.add('sized-container'); // โ Alternating reads/writes (reflow thrashing) for (let i = 0; i < 10; i++) { element.style.width = (element.offsetWidth + 10) + 'px'; } // โ Separate reads and writes let width = element.offsetWidth; for (let i = 0; i < 10; i++) { width += 10; } element.style.width = width + 'px';
// โ Transform (GPU accelerated) element.style.transform = 'translateZ(0)'; // Force GPU element.style.transform = 'translate(100px, 100px)'; // โ Opacity (GPU accelerated with will-change) element.style.willChange = 'opacity'; element.style.opacity = 0.5; // โ Filter (GPU accelerated in modern browsers) element.style.filter = 'blur(5px)'; // โ Avoid: Left/Top repositioning (triggers layout) element.style.left = '100px'; element.style.top = '100px';
<!-- โ Lazy load images below the fold --> <img src="hero.jpg" alt="Hero" loading="lazy"> <!-- โ Responsive images --> <img srcset="small.jpg 500w, large.jpg 1200w" sizes="100vw" src="medium.jpg" alt="Image"> <!-- โ Web font optimization --> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> <link rel="preconnect" href="//fonts.googleapis.com"> <!-- โ Font-display strategy --> <style> @font-face { font-family: 'MyFont'; src: url('font.woff2') format('woff2'); font-display: swap; /* Show fallback immediately, swap when ready */ } </style>
// 1. Open DevTools (F12) -> Performance tab // 2. Click "Start profiling and reload page" // 3. Look for red bars in the timeline (blocking resources) // Key indicators: // - Long green bars = JavaScript execution (blocking) // - Yellow bars = CSS parsing (blocking) // - Blue bars = HTML parsing (non-blocking) // - Purple bars = Layout/Reflow (expensive operations)
// 1. Open DevTools -> Network tab // 2. Check "Disable cache" for first-visit simulation // 3. Look for "Blocking" column (shows render-blocking time) // Render-blocking indicators: // - CSS files with "Blocking: Yes" // - JS files with "Blocking: Yes" (default scripts) // - Fonts with "Blocking: Yes" (FOIT scenarios)
// Run Lighthouse audit: // 1. DevTools -> Lighthouse tab // 2. Check "Performance" category // 3. Look for: // - "Eliminate render-blocking resources" // - "Reduce unused CSS" // - "Remove unused JavaScript" // Example Lighthouse output: // โ Eliminate render-blocking resources (2.1s) // - styles.css (1.2s) // - app.js (0.9s)
// Measure First Contentful Paint (FCP) new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('FCP:', entry.startTime + 'ms'); } }).observe({ entryTypes: ['paint'] }); // Measure Largest Contentful Paint (LCP) new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('LCP:', entry.startTime + 'ms'); } }).observe({ entryTypes: ['largest-contentful-paint'] }); // Track resource loading times window.addEventListener('load', () => { const resources = performance.getEntriesByType('resource'); resources.forEach(resource => { if (resource.name.includes('.css') || resource.name.includes('.js')) { console.log(`${resource.name}: ${resource.responseEnd - resource.requestStart}ms`); } }); });
// Track render-blocking impact across users // Example: Send to analytics when FCP > 2.5s new PerformanceObserver((list) => { const fcp = list.getEntries()[0]; if (fcp.startTime > 2500) { // Send to analytics gtag('event', 'web_vitals', { event_category: 'Web Vitals', event_label: 'FCP', value: Math.round(fcp.startTime), custom_map: { metric_value: fcp.startTime } }); } }).observe({ entryTypes: ['paint'] });
// โ Blocking: Large bundle loads immediately import React from 'react'; import { createRoot } from 'react-dom/client'; // ... 500KB of components ... // โ Better: Code splitting with lazy loading import React, { Suspense, lazy } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <HeavyComponent /> </Suspense> ); } // โ Best: Preload critical routes const preloadRoute = (route) => { const link = document.createElement('link'); link.rel = 'prefetch'; link.href = route; document.head.appendChild(link); };
// โ Async component loading const AsyncComponent = () => import('./AsyncComponent.vue'); // โ Route-based code splitting const router = createRouter({ routes: [ { path: '/heavy-page', component: () => import('./HeavyPage.vue'), meta: { preload: true } } ] });
// โ Lazy loading modules const routes: Routes = [ { path: 'feature', loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule) } ]; // โ Preloading strategies @NgModule({ imports: [ RouterModule.forRoot(routes, { preloadingStrategy: QuicklinkStrategy // or PreloadAllModules }) ] }) export class AppModule { }
// Mobile devices have: // - Slower CPU (2-4x slower than desktop) // - Higher latency networks // - Smaller memory // - Battery constraints // Impact on render-blocking: // - CSS parsing 3x slower on mobile // - JavaScript execution 2-4x slower // - Network requests have higher latency
<!-- โ Critical CSS only (mobile-first) --> <style> /* Only essential mobile styles */ body { font-size: 16px; margin: 0; } .mobile-nav { display: block; } @media (min-width: 768px) { .mobile-nav { display: none; } } </style> <!-- โ Defer desktop-specific CSS --> <link rel="stylesheet" href="desktop.css" media="(min-width: 768px)"> <!-- โ Smaller JavaScript bundles for mobile --> <script src="mobile-app.js" defer></script> <script src="desktop-enhancements.js" defer media="(min-width: 768px)"></script>
<!-- DNS prefetch for external domains --> <link rel="dns-prefetch" href="//cdn.example.com"> <!-- Preconnect for critical connections --> <link rel="preconnect" href="//fonts.googleapis.com" crossorigin> <!-- Preload critical resources --> <link rel="preload" href="critical.css" as="style"> <link rel="preload" href="hero-image.webp" as="image"> <!-- Prefetch for likely next pages --> <link rel="prefetch" href="/next-page.html">
// Cache critical resources for instant loading self.addEventListener('install', (event) => { event.waitUntil( caches.open('critical-v1').then((cache) => { return cache.addAll([ '/critical.css', '/critical.js', '/hero-image.webp' ]); }) ); }); // Serve cached resources instantly self.addEventListener('fetch', (event) => { if (event.request.url.includes('critical')) { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request); }) ); } });
// Load non-critical CSS when needed const loadNonCriticalCSS = () => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'non-critical.css'; document.head.appendChild(link); }; // Load when user scrolls near content const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadNonCriticalCSS(); observer.disconnect(); } }); }); observer.observe(document.querySelector('.content-section'));
Before:
<!-- Blocking CSS --> <link rel="stylesheet" href="bootstrap.css"> <!-- 200KB --> <link rel="stylesheet" href="theme.css"> <!-- 150KB --> <link rel="stylesheet" href="components.css"> <!-- 100KB --> <!-- Blocking JavaScript --> <script src="jquery.js"></script> <script src="bootstrap.js"></script> <script src="app.js"></script>
After:
<!-- Critical CSS inline --> <style> /* 14KB of critical styles only */ body { margin: 0; font-family: Arial; } .hero { background: #f0f0f0; padding: 2rem; } .product-card { border: 1px solid #ddd; } </style> <!-- Defer non-critical CSS --> <link rel="preload" href="all-styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <!-- Defer JavaScript --> <script src="jquery.js" defer></script> <script src="bootstrap.js" defer></script> <script src="app.js" defer></script>
Results:
Problem: Heavy third-party scripts blocking rendering.
Solution:
<!-- Move all third-party to end of body --> <body> <!-- Page content loads first --> <header>...</header> <main>...</main> <!-- Third-party scripts at end --> <script async src="//ads.example.com/ads.js"></script> <script async src="//analytics.example.com/tracking.js"></script> <script async src="//social.example.com/widgets.js"></script> </body>
Results:
defer for most scripts, async for analyticsasync, consider loading at end of pagegeneral/browser_rendering.md) - Visual flow of rendering stagesjs/promises/) - Understand async JavaScript patternsjs/general-concepts/event_loop.md) - Deep understanding of microtasks, macrotasks, and rendering synchronization<!-- Preload pages based on user behavior patterns --> <script type="speculationrules"> { "prerender": [ { "source": "document", "where": { "href_matches": "/*" }, "eagerness": "moderate" } ] } </script>
<!-- Set resource priorities --> <link rel="stylesheet" href="critical.css" fetchpriority="high"> <link rel="stylesheet" href="non-critical.css" fetchpriority="low"> <img src="hero.jpg" fetchpriority="high"> <img src="background.jpg" fetchpriority="low">
/* Skip rendering off-screen content */ .content-section { content-visibility: auto; contain-intrinsic-size: 0 500px; /* Estimated size */ }
/* Organize CSS without specificity wars */ @layer reset, base, components, utilities; @layer base { body { margin: 0; } } @layer components { .card { border: 1px solid #ddd; } }
Test your understanding with 3 quick questions