Files of Resource loading and critical rendering path
wondelai/
Show the full text422 lines
Resource Loading and Critical Rendering Path
The browser converts raw bytes into rendered pixels through a precise pipeline. Understanding this pipeline reveals exactly which resources block rendering and how to eliminate or defer them.
Table of Contents
- The Critical Rendering Path
- Render-Blocking Resources
- Async and Defer
- Resource Hints
- Font Loading Strategies
- Image Optimization
- Resource Loading Priority
- Practical Loading Strategy
The Critical Rendering Path
Every page load follows this sequence:
HTML bytes → Parse → DOM
↓
CSS bytes → Parse → CSSOM
↓
Render Tree (DOM + CSSOM)
↓
Layout (geometry calculation)
↓
Paint (pixel rendering)
↓
Composite (layer assembly)
The key insight: The browser cannot paint anything until both the DOM and CSSOM are complete. CSS is render-blocking. JavaScript is parser-blocking. Every blocking resource directly adds to time-to-first-paint.
DOM Construction
The browser receives HTML bytes, decodes them into characters, tokenizes them into tags, and builds the DOM tree. This is incremental -- the parser processes HTML as it arrives, building the tree progressively.
When parsing stops:
- A
<script>tag withoutasyncordeferhalts DOM construction. The browser must download the script, execute it (because it might calldocument.write()), and then resume parsing. - The parser does not stop for CSS files, but rendering is blocked until CSS is parsed.
CSSOM Construction
CSS files are fetched and parsed into the CSSOM (CSS Object Model). Unlike DOM construction:
- CSSOM is not incremental -- the browser waits for all CSS before computing styles
- CSS is render-blocking but not parser-blocking (DOM construction continues)
- A
<script>after a<link rel="stylesheet">is blocked until the CSS loads (because the script might query computed styles)
Render Tree
The render tree combines the DOM and CSSOM, containing only visible elements with their computed styles. Elements with display: none are excluded. The render tree triggers:
- Layout (reflow): Calculates the exact position and size of each element
- Paint: Fills in pixels -- text, colors, borders, shadows, images
- Composite: Assembles painted layers into the final image (GPU-accelerated for transformed/animated elements)
Render-Blocking Resources
CSS is render-blocking
The browser will not render any content until all CSS in the <head> is loaded and parsed. This means:
<!-- This CSS file blocks ALL rendering until loaded -->
<link rel="stylesheet" href="styles.css">
Optimization strategies:
Inline critical CSS: Extract the styles needed for above-the-fold content and embed them directly in the HTML:
<head>
<style>
/* Critical styles for above-the-fold content */
body { font-family: system-ui; margin: 0; }
.hero { padding: 2rem; background: #f8f9fa; }
.nav { display: flex; gap: 1rem; }
</style>
<!-- Load full stylesheet asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
Media queries for conditional CSS: Stylesheets with media queries that do not match the current context are downloaded but do not block rendering:
<!-- Blocks rendering (matches all screens) -->
<link rel="stylesheet" href="styles.css">
<!-- Does NOT block rendering on screen (only for print) -->
<link rel="stylesheet" href="print.css" media="print">
<!-- Does NOT block rendering below 768px -->
<link rel="stylesheet" href="desktop.css" media="(min-width: 768px)">
Remove unused CSS: Audit CSS with Chrome DevTools Coverage panel. Typical sites ship 60-90% unused CSS.
JavaScript is parser-blocking
A <script> tag without attributes halts DOM parsing:
<p>This paragraph is parsed</p>
<script src="app.js"></script>
<!-- DOM parsing stops until app.js downloads AND executes -->
<p>This paragraph waits</p>
Async and Defer
The async and defer attributes change how scripts interact with the parser:
defer
<script src="app.js" defer></script>
- Downloads in parallel with DOM parsing (non-blocking)
- Executes after DOM parsing is complete, before
DOMContentLoaded - Multiple deferred scripts execute in document order
- Best for: Most application scripts that depend on the DOM
async
<script src="analytics.js" async></script>
- Downloads in parallel with DOM parsing (non-blocking)
- Executes immediately when download completes (may interrupt parsing)
- No guaranteed execution order between multiple async scripts
- Best for: Independent scripts that do not depend on DOM or other scripts (analytics, ads)
Comparison
| Attribute | Download | Execution | Order guaranteed | Blocks parsing |
|---|---|---|---|---|
| None | Sequential | Immediately | Yes | Yes |
async |
Parallel | When ready | No | Briefly (during execution) |
defer |
Parallel | After DOM parsed | Yes | No |
Module scripts
<script type="module" src="app.mjs"></script>
Module scripts are deferred by default. Adding async to a module script makes it execute as soon as its dependency graph is resolved.
Resource Hints
Resource hints allow developers to inform the browser about resources it will need, enabling earlier loading.
dns-prefetch
Resolves the DNS for a domain before any request is made:
<link rel="dns-prefetch" href="https://cdn.example.com">
- Cheapest hint -- only does DNS resolution
- Use for third-party domains (analytics, fonts, CDNs)
- Saves 20-120ms per domain
preconnect
Resolves DNS, establishes TCP connection, and negotiates TLS:
<link rel="preconnect" href="https://cdn.example.com">
- More expensive than
dns-prefetch(holds a connection open) - Use for origins you will definitely request from within seconds
- Limit to 2-4 critical origins to avoid wasting connections
- Always include
crossoriginfor CORS-enabled resources:
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
preload
Fetches a specific resource at high priority without blocking rendering:
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">
<link rel="preload" href="/critical.css" as="style">
- The
asattribute is required -- it sets the correct priority and CORS mode - Fetches immediately at high priority
- Does not apply the resource (does not render CSS, execute JS, etc.)
- Use for critical resources discovered late in the HTML (fonts referenced in CSS, images in CSS backgrounds)
- Warning: Preloading too many resources dilutes the benefit -- limit to truly critical resources (LCP image, critical fonts)
prefetch
Fetches a resource at low priority for a likely future navigation:
<link rel="prefetch" href="/next-page.html">
<link rel="prefetch" href="/next-page-data.json">
- Downloaded at lowest priority during idle time
- Stored in the HTTP cache for future use
- Use for resources needed on the next likely page (next step in a flow, frequently visited page)
- Does not work if the user has Data Saver enabled
prerender / Speculation Rules
Modern browsers support the Speculation Rules API for prerendering entire pages:
<script type="speculationrules">
{
"prerender": [
{ "urls": ["/likely-next-page"] }
]
}
</script>
- Renders the entire page in a hidden tab
- Near-instant navigation when the user clicks
- Expensive in terms of memory and bandwidth -- use only for high-confidence predictions
Font Loading Strategies
Web fonts create unique performance challenges because the browser may hide text while fonts load (Flash of Invisible Text, or FOIT) or show a jarring swap (Flash of Unstyled Text, or FOUT).
font-display values
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when ready */
}
| Value | Behavior | Best for |
|---|---|---|
auto |
Browser default (usually FOIT for 3s) | Not recommended |
block |
Invisible text for up to 3s, then swap | Icon fonts only |
swap |
Show fallback immediately, swap when loaded | Body text, most use cases |
fallback |
Brief invisible period (100ms), then fallback, then swap | Balanced approach |
optional |
Brief invisible period, uses font only if cached | Performance-critical; accepts FOUT |
Preloading fonts
Fonts referenced in CSS are discovered late -- the browser must load HTML, then CSS, then discover the font URL. Preloading jumps the queue:
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
The crossorigin attribute is required even for same-origin fonts because the font specification requires CORS.
Font subsetting
Subset fonts to include only the characters needed:
- Latin-only sites can drop CJK, Cyrillic, and other ranges
- Use
unicode-rangein@font-faceto load character ranges on demand
@font-face {
font-family: 'Custom Font';
src: url('font-latin.woff2') format('woff2');
unicode-range: U+0000-00FF; /* Basic Latin */
}
Image Optimization
Images typically account for 50-70% of total page weight.
Lazy loading
<!-- Browser-native lazy loading -->
<img src="photo.jpg" loading="lazy" alt="Description">
<!-- Do NOT lazy-load above-fold images (LCP candidates) -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero">
Responsive images
Serve appropriately sized images for each viewport:
<img
srcset="photo-400.webp 400w,
photo-800.webp 800w,
photo-1200.webp 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
src="photo-800.webp"
alt="Description"
>
Modern formats
| Format | Compression | Browser support | Best for |
|---|---|---|---|
| AVIF | Best (50% smaller than JPEG) | Chrome, Firefox, Safari 16+ | Photos, complex images |
| WebP | Good (25-35% smaller than JPEG) | All modern browsers | General purpose |
| JPEG | Baseline | Universal | Fallback |
| PNG | Lossless | Universal | Transparency, screenshots |
| SVG | Vector (tiny for icons) | Universal | Icons, logos, illustrations |
Use the <picture> element for format negotiation:
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="Description">
</picture>
Explicit dimensions
Always set width and height (or use CSS aspect-ratio) to prevent layout shift:
<img src="photo.jpg" width="800" height="600" alt="Description">
img {
aspect-ratio: 4 / 3;
width: 100%;
height: auto;
}
Resource Loading Priority
Modern browsers assign priorities based on resource type and position. The fetchpriority attribute gives developers explicit control:
<!-- High priority for LCP image -->
<img src="hero.webp" fetchpriority="high" alt="Hero">
<!-- Low priority for below-fold image -->
<img src="footer-bg.webp" fetchpriority="low" loading="lazy" alt="">
<!-- High priority for critical script -->
<script src="critical.js" fetchpriority="high"></script>
Default browser priorities
| Resource | Default priority |
|---|---|
| HTML document | Highest |
CSS in <head> |
Highest |
| Preloaded resources | High |
Scripts in <head> |
High |
| Images in viewport | High |
| Scripts at end of body | Medium |
| Images out of viewport | Low |
| Prefetched resources | Lowest |
Practical Loading Strategy
A complete resource loading strategy for a typical page:
<head>
<!-- 1. DNS prefetch for third-party origins -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<!-- 2. Preconnect to critical origins -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 3. Preload critical resources discovered late -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image">
<!-- 4. Inline critical CSS -->
<style>/* above-fold styles */</style>
<!-- 5. Async load full stylesheet -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<!-- 6. Deferred application scripts -->
<script src="/app.js" defer></script>
<!-- 7. Async independent scripts -->
<script src="/analytics.js" async></script>
</head>
<body>
<!-- 8. LCP element with high priority -->
<img src="/hero.avif" fetchpriority="high" alt="Hero" width="1200" height="600">
<!-- 9. Below-fold images lazy loaded -->
<img src="/feature.webp" loading="lazy" alt="Feature" width="800" height="400">
<!-- 10. Prefetch next page for likely navigation -->
<link rel="prefetch" href="/next-page.html">
</body>
This ordering ensures critical resources load first, nothing blocks rendering unnecessarily, and resources for future navigations are prepared during idle time.
| 1 | # Resource Loading and Critical Rendering Path |
| 2 | |
| 3 | The browser converts raw bytes into rendered pixels through a precise pipeline. Understanding this pipeline reveals exactly which resources block rendering and how to eliminate or defer them. |
| 4 | |
| 5 | |
| 6 | ## Table of Contents |
| 7 | [The Critical Rendering Path] |
| 8 | [Render-Blocking Resources] |
| 9 | [Async and Defer] |
| 10 | [Resource Hints] |
| 11 | [Font Loading Strategies] |
| 12 | [Image Optimization] |
| 13 | [Resource Loading Priority] |
| 14 | [Practical Loading Strategy] |
| 15 | |
| 16 | |
| 17 | |
| 18 | ## The Critical Rendering Path |
| 19 | |
| 20 | Every page load follows this sequence: |
| 21 | |
| 22 | |
| 23 | HTML bytes → Parse → DOM |
| 24 | ↓ |
| 25 | CSS bytes → Parse → CSSOM |
| 26 | ↓ |
| 27 | Render Tree (DOM + CSSOM) |
| 28 | ↓ |
| 29 | Layout (geometry calculation) |
| 30 | ↓ |
| 31 | Paint (pixel rendering) |
| 32 | ↓ |
| 33 | Composite (layer assembly) |
| 34 | |
| 35 | |
| 36 | **The key insight:** The browser cannot paint anything until both the DOM and CSSOM are complete. CSS is **render-blocking**. JavaScript is **parser-blocking**. Every blocking resource directly adds to time-to-first-paint. |
| 37 | |
| 38 | ### DOM Construction |
| 39 | |
| 40 | The browser receives HTML bytes, decodes them into characters, tokenizes them into tags, and builds the DOM tree. This is incremental -- the parser processes HTML as it arrives, building the tree progressively. |
| 41 | |
| 42 | **When parsing stops:** |
| 43 | A `<script>` tag without `async` or `defer` halts DOM construction. The browser must download the script, execute it (because it might call `document.write()`), and then resume parsing. |
| 44 | The parser does not stop for CSS files, but rendering is blocked until CSS is parsed. |
| 45 | |
| 46 | ### CSSOM Construction |
| 47 | |
| 48 | CSS files are fetched and parsed into the CSSOM (CSS Object Model). Unlike DOM construction: |
| 49 | CSSOM is not incremental -- the browser waits for all CSS before computing styles |
| 50 | CSS is render-blocking but not parser-blocking (DOM construction continues) |
| 51 | A `<script>` after a `<link rel="stylesheet">` is blocked until the CSS loads (because the script might query computed styles) |
| 52 | |
| 53 | ### Render Tree |
| 54 | |
| 55 | The render tree combines the DOM and CSSOM, containing only visible elements with their computed styles. Elements with `display: none` are excluded. The render tree triggers: |
| 56 | |
| 57 | **Layout (reflow):** Calculates the exact position and size of each element |
| 58 | **Paint:** Fills in pixels -- text, colors, borders, shadows, images |
| 59 | **Composite:** Assembles painted layers into the final image (GPU-accelerated for transformed/animated elements) |
| 60 | |
| 61 | ## Render-Blocking Resources |
| 62 | |
| 63 | ### CSS is render-blocking |
| 64 | |
| 65 | The browser will not render any content until all CSS in the `<head>` is loaded and parsed. This means: |
| 66 | |
| 67 | |
| 68 | <!-- This CSS file blocks ALL rendering until loaded --> |
| 69 | <link rel="stylesheet" href="styles.css"> |
| 70 | |
| 71 | |
| 72 | **Optimization strategies:** |
| 73 | |
| 74 | **Inline critical CSS:** Extract the styles needed for above-the-fold content and embed them directly in the HTML: |
| 75 | |
| 76 | |
| 77 | <head> |
| 78 | <style> |
| 79 | /* Critical styles for above-the-fold content */ |
| 80 | body { font-family: system-ui; margin: 0; } |
| 81 | .hero { padding: 2rem; background: #f8f9fa; } |
| 82 | .nav { display: flex; gap: 1rem; } |
| 83 | </style> |
| 84 | <!-- Load full stylesheet asynchronously --> |
| 85 | <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> |
| 86 | <noscript><link rel="stylesheet" href="styles.css"></noscript> |
| 87 | </head> |
| 88 | |
| 89 | |
| 90 | **Media queries for conditional CSS:** Stylesheets with media queries that do not match the current context are downloaded but do not block rendering: |
| 91 | |
| 92 | |
| 93 | <!-- Blocks rendering (matches all screens) --> |
| 94 | <link rel="stylesheet" href="styles.css"> |
| 95 | |
| 96 | <!-- Does NOT block rendering on screen (only for print) --> |
| 97 | <link rel="stylesheet" href="print.css" media="print"> |
| 98 | |
| 99 | <!-- Does NOT block rendering below 768px --> |
| 100 | <link rel="stylesheet" href="desktop.css" media="(min-width: 768px)"> |
| 101 | |
| 102 | |
| 103 | **Remove unused CSS:** Audit CSS with Chrome DevTools Coverage panel. Typical sites ship 60-90% unused CSS. |
| 104 | |
| 105 | ### JavaScript is parser-blocking |
| 106 | |
| 107 | A `<script>` tag without attributes halts DOM parsing: |
| 108 | |
| 109 | |
| 110 | <p>This paragraph is parsed</p> |
| 111 | <script src="app.js"></script> |
| 112 | <!-- DOM parsing stops until app.js downloads AND executes --> |
| 113 | <p>This paragraph waits</p> |
| 114 | |
| 115 | |
| 116 | ## Async and Defer |
| 117 | |
| 118 | The `async` and `defer` attributes change how scripts interact with the parser: |
| 119 | |
| 120 | ### `defer` |
| 121 | |
| 122 | |
| 123 | <script src="app.js" defer></script> |
| 124 | |
| 125 | |
| 126 | Downloads in parallel with DOM parsing (non-blocking) |
| 127 | Executes **after** DOM parsing is complete, before `DOMContentLoaded` |
| 128 | Multiple deferred scripts execute in document order |
| 129 | **Best for:** Most application scripts that depend on the DOM |
| 130 | |
| 131 | ### `async` |
| 132 | |
| 133 | |
| 134 | <script src="analytics.js" async></script> |
| 135 | |
| 136 | |
| 137 | Downloads in parallel with DOM parsing (non-blocking) |
| 138 | Executes **immediately** when download completes (may interrupt parsing) |
| 139 | No guaranteed execution order between multiple async scripts |
| 140 | **Best for:** Independent scripts that do not depend on DOM or other scripts (analytics, ads) |
| 141 | |
| 142 | ### Comparison |
| 143 | |
| 144 | | Attribute | Download | Execution | Order guaranteed | Blocks parsing | |
| 145 | |-----------|----------|-----------|-----------------|----------------| |
| 146 | | None | Sequential | Immediately | Yes | Yes | |
| 147 | | `async` | Parallel | When ready | No | Briefly (during execution) | |
| 148 | | `defer` | Parallel | After DOM parsed | Yes | No | |
| 149 | |
| 150 | ### Module scripts |
| 151 | |
| 152 | |
| 153 | <script type="module" src="app.mjs"></script> |
| 154 | |
| 155 | |
| 156 | Module scripts are deferred by default. Adding `async` to a module script makes it execute as soon as its dependency graph is resolved. |
| 157 | |
| 158 | ## Resource Hints |
| 159 | |
| 160 | Resource hints allow developers to inform the browser about resources it will need, enabling earlier loading. |
| 161 | |
| 162 | ### `dns-prefetch` |
| 163 | |
| 164 | Resolves the DNS for a domain before any request is made: |
| 165 | |
| 166 | |
| 167 | <link rel="dns-prefetch" href="https://cdn.example.com"> |
| 168 | |
| 169 | |
| 170 | Cheapest hint -- only does DNS resolution |
| 171 | Use for third-party domains (analytics, fonts, CDNs) |
| 172 | Saves 20-120ms per domain |
| 173 | |
| 174 | ### `preconnect` |
| 175 | |
| 176 | Resolves DNS, establishes TCP connection, and negotiates TLS: |
| 177 | |
| 178 | |
| 179 | <link rel="preconnect" href="https://cdn.example.com"> |
| 180 | |
| 181 | |
| 182 | More expensive than `dns-prefetch` (holds a connection open) |
| 183 | Use for origins you will definitely request from within seconds |
| 184 | Limit to 2-4 critical origins to avoid wasting connections |
| 185 | Always include `crossorigin` for CORS-enabled resources: |
| 186 | |
| 187 | |
| 188 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 189 | |
| 190 | |
| 191 | ### `preload` |
| 192 | |
| 193 | Fetches a specific resource at high priority without blocking rendering: |
| 194 | |
| 195 | |
| 196 | <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin> |
| 197 | <link rel="preload" href="/hero.webp" as="image"> |
| 198 | <link rel="preload" href="/critical.css" as="style"> |
| 199 | |
| 200 | |
| 201 | **The `as` attribute is required** -- it sets the correct priority and CORS mode |
| 202 | Fetches immediately at high priority |
| 203 | Does not apply the resource (does not render CSS, execute JS, etc.) |
| 204 | Use for critical resources discovered late in the HTML (fonts referenced in CSS, images in CSS backgrounds) |
| 205 | **Warning:** Preloading too many resources dilutes the benefit -- limit to truly critical resources (LCP image, critical fonts) |
| 206 | |
| 207 | ### `prefetch` |
| 208 | |
| 209 | Fetches a resource at low priority for a likely future navigation: |
| 210 | |
| 211 | |
| 212 | <link rel="prefetch" href="/next-page.html"> |
| 213 | <link rel="prefetch" href="/next-page-data.json"> |
| 214 | |
| 215 | |
| 216 | Downloaded at lowest priority during idle time |
| 217 | Stored in the HTTP cache for future use |
| 218 | Use for resources needed on the next likely page (next step in a flow, frequently visited page) |
| 219 | Does not work if the user has Data Saver enabled |
| 220 | |
| 221 | ### `prerender` / Speculation Rules |
| 222 | |
| 223 | Modern browsers support the Speculation Rules API for prerendering entire pages: |
| 224 | |
| 225 | |
| 226 | <script type="speculationrules"> |
| 227 | { |
| 228 | "prerender": [ |
| 229 | { "urls": ["/likely-next-page"] } |
| 230 | ] |
| 231 | } |
| 232 | </script> |
| 233 | |
| 234 | |
| 235 | Renders the entire page in a hidden tab |
| 236 | Near-instant navigation when the user clicks |
| 237 | Expensive in terms of memory and bandwidth -- use only for high-confidence predictions |
| 238 | |
| 239 | ## Font Loading Strategies |
| 240 | |
| 241 | Web fonts create unique performance challenges because the browser may hide text while fonts load (Flash of Invisible Text, or FOIT) or show a jarring swap (Flash of Unstyled Text, or FOUT). |
| 242 | |
| 243 | ### `font-display` values |
| 244 | |
| 245 | |
| 246 | @font-face { |
| 247 | font-family: 'Custom Font'; |
| 248 | src: url('font.woff2') format('woff2'); |
| 249 | font-display: swap; /* Show fallback immediately, swap when ready */ |
| 250 | } |
| 251 | |
| 252 | |
| 253 | | Value | Behavior | Best for | |
| 254 | |-------|----------|----------| |
| 255 | | `auto` | Browser default (usually FOIT for 3s) | Not recommended | |
| 256 | | `block` | Invisible text for up to 3s, then swap | Icon fonts only | |
| 257 | | `swap` | Show fallback immediately, swap when loaded | Body text, most use cases | |
| 258 | | `fallback` | Brief invisible period (100ms), then fallback, then swap | Balanced approach | |
| 259 | | `optional` | Brief invisible period, uses font only if cached | Performance-critical; accepts FOUT | |
| 260 | |
| 261 | ### Preloading fonts |
| 262 | |
| 263 | Fonts referenced in CSS are discovered late -- the browser must load HTML, then CSS, then discover the font URL. Preloading jumps the queue: |
| 264 | |
| 265 | |
| 266 | <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin> |
| 267 | |
| 268 | |
| 269 | The `crossorigin` attribute is required even for same-origin fonts because the font specification requires CORS. |
| 270 | |
| 271 | ### Font subsetting |
| 272 | |
| 273 | Subset fonts to include only the characters needed: |
| 274 | Latin-only sites can drop CJK, Cyrillic, and other ranges |
| 275 | Use `unicode-range` in `@font-face` to load character ranges on demand |
| 276 | |
| 277 | |
| 278 | @font-face { |
| 279 | font-family: 'Custom Font'; |
| 280 | src: url('font-latin.woff2') format('woff2'); |
| 281 | unicode-range: U+0000-00FF; /* Basic Latin */ |
| 282 | } |
| 283 | |
| 284 | |
| 285 | ## Image Optimization |
| 286 | |
| 287 | Images typically account for 50-70% of total page weight. |
| 288 | |
| 289 | ### Lazy loading |
| 290 | |
| 291 | |
| 292 | <!-- Browser-native lazy loading --> |
| 293 | <img src="photo.jpg" loading="lazy" alt="Description"> |
| 294 | |
| 295 | <!-- Do NOT lazy-load above-fold images (LCP candidates) --> |
| 296 | <img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero"> |
| 297 | |
| 298 | |
| 299 | ### Responsive images |
| 300 | |
| 301 | Serve appropriately sized images for each viewport: |
| 302 | |
| 303 | |
| 304 | <img |
| 305 | srcset="photo-400.webp 400w, |
| 306 | photo-800.webp 800w, |
| 307 | photo-1200.webp 1200w" |
| 308 | sizes="(max-width: 600px) 400px, |
| 309 | (max-width: 1000px) 800px, |
| 310 | 1200px" |
| 311 | src="photo-800.webp" |
| 312 | alt="Description" |
| 313 | > |
| 314 | |
| 315 | |
| 316 | ### Modern formats |
| 317 | |
| 318 | | Format | Compression | Browser support | Best for | |
| 319 | |--------|-------------|-----------------|----------| |
| 320 | | **AVIF** | Best (50% smaller than JPEG) | Chrome, Firefox, Safari 16+ | Photos, complex images | |
| 321 | | **WebP** | Good (25-35% smaller than JPEG) | All modern browsers | General purpose | |
| 322 | | **JPEG** | Baseline | Universal | Fallback | |
| 323 | | **PNG** | Lossless | Universal | Transparency, screenshots | |
| 324 | | **SVG** | Vector (tiny for icons) | Universal | Icons, logos, illustrations | |
| 325 | |
| 326 | Use the `<picture>` element for format negotiation: |
| 327 | |
| 328 | |
| 329 | <picture> |
| 330 | <source srcset="photo.avif" type="image/avif"> |
| 331 | <source srcset="photo.webp" type="image/webp"> |
| 332 | <img src="photo.jpg" alt="Description"> |
| 333 | </picture> |
| 334 | |
| 335 | |
| 336 | ### Explicit dimensions |
| 337 | |
| 338 | Always set width and height (or use CSS `aspect-ratio`) to prevent layout shift: |
| 339 | |
| 340 | |
| 341 | <img src="photo.jpg" width="800" height="600" alt="Description"> |
| 342 | |
| 343 | |
| 344 | |
| 345 | img { |
| 346 | aspect-ratio: 4 / 3; |
| 347 | width: 100%; |
| 348 | height: auto; |
| 349 | } |
| 350 | |
| 351 | |
| 352 | ## Resource Loading Priority |
| 353 | |
| 354 | Modern browsers assign priorities based on resource type and position. The `fetchpriority` attribute gives developers explicit control: |
| 355 | |
| 356 | |
| 357 | <!-- High priority for LCP image --> |
| 358 | <img src="hero.webp" fetchpriority="high" alt="Hero"> |
| 359 | |
| 360 | <!-- Low priority for below-fold image --> |
| 361 | <img src="footer-bg.webp" fetchpriority="low" loading="lazy" alt=""> |
| 362 | |
| 363 | <!-- High priority for critical script --> |
| 364 | <script src="critical.js" fetchpriority="high"></script> |
| 365 | |
| 366 | |
| 367 | ### Default browser priorities |
| 368 | |
| 369 | | Resource | Default priority | |
| 370 | |----------|-----------------| |
| 371 | | HTML document | Highest | |
| 372 | | CSS in `<head>` | Highest | |
| 373 | | Preloaded resources | High | |
| 374 | | Scripts in `<head>` | High | |
| 375 | | Images in viewport | High | |
| 376 | | Scripts at end of body | Medium | |
| 377 | | Images out of viewport | Low | |
| 378 | | Prefetched resources | Lowest | |
| 379 | |
| 380 | ## Practical Loading Strategy |
| 381 | |
| 382 | A complete resource loading strategy for a typical page: |
| 383 | |
| 384 | |
| 385 | <head> |
| 386 | <!-- 1. DNS prefetch for third-party origins --> |
| 387 | <link rel="dns-prefetch" href="https://analytics.example.com"> |
| 388 | |
| 389 | <!-- 2. Preconnect to critical origins --> |
| 390 | <link rel="preconnect" href="https://cdn.example.com"> |
| 391 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 392 | |
| 393 | <!-- 3. Preload critical resources discovered late --> |
| 394 | <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin> |
| 395 | <link rel="preload" href="/hero.avif" as="image"> |
| 396 | |
| 397 | <!-- 4. Inline critical CSS --> |
| 398 | <style>/* above-fold styles */</style> |
| 399 | |
| 400 | <!-- 5. Async load full stylesheet --> |
| 401 | <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> |
| 402 | |
| 403 | <!-- 6. Deferred application scripts --> |
| 404 | <script src="/app.js" defer></script> |
| 405 | |
| 406 | <!-- 7. Async independent scripts --> |
| 407 | <script src="/analytics.js" async></script> |
| 408 | </head> |
| 409 | <body> |
| 410 | <!-- 8. LCP element with high priority --> |
| 411 | <img src="/hero.avif" fetchpriority="high" alt="Hero" width="1200" height="600"> |
| 412 | |
| 413 | <!-- 9. Below-fold images lazy loaded --> |
| 414 | <img src="/feature.webp" loading="lazy" alt="Feature" width="800" height="400"> |
| 415 | |
| 416 | <!-- 10. Prefetch next page for likely navigation --> |
| 417 | <link rel="prefetch" href="/next-page.html"> |
| 418 | </body> |
| 419 | |
| 420 | |
| 421 | This ordering ensures critical resources load first, nothing blocks rendering unnecessarily, and resources for future navigations are prepared during idle time. |
| 422 |
Discussion
Alternatives
Browse more free Claude skills or everything in Sales.