HTML Performance
HTML performance optimization encompasses the strategies and techniques used to deliver web pages as quickly and efficiently as possible. Every millisecond of loading time affects user experience, engagement, and conversion rates.
Performance starts at the HTML layer. The structure of your markup, the order of your resources, and the attributes you apply to elements directly influence how browsers parse, render, and interact with your page. Understanding the browser's rendering pipeline is essential to making informed optimization decisions.
info
The Critical Rendering Path (CRP) is the sequence of steps the browser goes through to convert HTML, CSS, and JavaScript into pixels on the screen. Understanding each step is crucial for identifying bottlenecks and optimizing page load.
JavaScript blocks DOM construction because it can modify the DOM. CSS blocks both DOM parsing (when scripts are present) and render tree construction. Delaying non-critical CSS and scripts is key to an optimized CRP.
| 1 | <!-- Inline critical CSS in <head> for first paint --> |
| 2 | <!DOCTYPE html> |
| 3 | <html lang="en"> |
| 4 | <head> |
| 5 | <meta charset="UTF-8"> |
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 7 | <title>Optimized Page</title> |
| 8 | <style> |
| 9 | /* Critical CSS — inlined for immediate paint */ |
| 10 | header, nav, .hero { display: block; } |
| 11 | body { margin: 0; font-family: system-ui; } |
| 12 | .hero { height: 60vh; background: #0A0A0A; } |
| 13 | </style> |
| 14 | <!-- Non-critical CSS loaded asynchronously --> |
| 15 | <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'"> |
| 16 | <noscript><link rel="stylesheet" href="styles.css"></noscript> |
| 17 | </head> |
| 18 | <body> |
| 19 | <header><!-- ... --></header> |
| 20 | <main class="hero"> |
| 21 | <h1>Performance Optimized</h1> |
| 22 | </main> |
| 23 | </body> |
| 24 | </html> |
best practice
HTML provides several <link> rel values that give the browser hints about how and when to load resources. These hints can significantly improve perceived performance by making the browser aware of important resources early in the loading process.
| Hint | Syntax | Behavior |
|---|---|---|
| preload | <link rel="preload"> | Fetches the resource as soon as the browser encounters the hint. Used for critical resources that would otherwise be discovered late. |
| prefetch | <link rel="prefetch"> | Fetches a resource for a future navigation. Lowest priority, cached and available for the next page load. |
| preconnect | <link rel="preconnect"> | Establishes an early connection (DNS + TCP + TLS) to an origin. Critical for third-party origins. |
| dns-prefetch | <link rel="dns-prefetch"> | Resolves DNS for an origin early. Less powerful than preconnect but broader browser support. |
| prerender | <link rel="prerender"> | Fetches and renders the entire page in the background. High resource cost — use sparingly. |
| modulepreload | <link rel="modulepreload"> | Preloads an ES module and its dependencies. Similar to preload but module-aware. |
| 1 | <!-- Preload: critical resources discovered late --> |
| 2 | <link rel="preload" href="fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin> |
| 3 | <link rel="preload" href="hero.jpg" as="image" imagesrcset="hero-640.jpg 640w, hero-1280.jpg 1280w" imagesizes="100vw"> |
| 4 | <link rel="preload" href="critical.js" as="script"> |
| 5 | <link rel="preload" href="critical.css" as="style"> |
| 6 | |
| 7 | <!-- Prefetch: resources for the next page --> |
| 8 | <link rel="prefetch" href="/products" as="document"> |
| 9 | <link rel="prefetch" href="/products.js" as="script"> |
| 10 | <link rel="prefetch" href="/products.css" as="style"> |
| 11 | |
| 12 | <!-- Preconnect: early connection to third-party origins --> |
| 13 | <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 14 | <link rel="preconnect" href="https://analytics.example.com"> |
| 15 | |
| 16 | <!-- dns-prefetch: lightweight DNS resolution fallback --> |
| 17 | <link rel="dns-prefetch" href="https://cdn.example.com"> |
| 18 | <link rel="dns-prefetch" href="https://images.example.com"> |
| 19 | |
| 20 | <!-- Prerender: full page render for likely next navigation --> |
| 21 | <link rel="prerender" href="/checkout"> |
| 22 | |
| 23 | <!-- Module preload: ES module with dependencies --> |
| 24 | <link rel="modulepreload" href="/modules/app.js"> |
warning
The async and defer attributes on <script> elements control how external scripts are fetched and executed relative to HTML parsing. Choosing the right attribute is one of the most impactful performance decisions.
| Attribute | Fetch Timing | Execution Timing | Order | Use Case |
|---|---|---|---|---|
| (none) | At parse position | Immediately after fetch | In document order | Legacy — blocks parser |
| async | In parallel with parsing | As soon as fetched | Unordered | Independent scripts (analytics, widgets) |
| defer | In parallel with parsing | After parsing completes | In document order | Scripts that need DOM access |
| 1 | <!-- Blocking script — pauses HTML parsing --> |
| 2 | <script src="legacy.js"></script> |
| 3 | |
| 4 | <!-- Async — fetched in parallel, executes ASAP --> |
| 5 | <script async src="analytics.js"></script> |
| 6 | <script async src="chat-widget.js"></script> |
| 7 | |
| 8 | <!-- Defer — fetched in parallel, executes after HTML parsed --> |
| 9 | <script defer src="app.js"></script> |
| 10 | <script defer src="components.js"></script> |
| 11 | <script defer src="utils.js"></script> |
| 12 | |
| 13 | <!-- Module scripts are deferred by default --> |
| 14 | <script type="module" src="main.js"></script> |
| 15 | <script type="module" src="router.js"></script> |
| 16 | |
| 17 | <!-- Inline scripts are always blocking — keep them small --> |
| 18 | <script> |
| 19 | // Critical inline script — blocks parsing briefly |
| 20 | window.__INITIAL_STATE__ = { user: null, theme: "dark" }; |
| 21 | </script> |
| 22 | |
| 23 | <!-- Nomodule fallback for legacy browsers --> |
| 24 | <script nomodule src="legacy-polyfill.js"></script> |
pro tip
Lazy loading defers the loading of non-critical resources until they are needed, typically when the user scrolls near them. Native lazy loading via the loading attribute is the simplest and most efficient approach for images and iframes.
| Attribute Value | Behavior | Supported Elements |
|---|---|---|
| lazy | Defers loading until the element approaches the viewport (typically within 1250px on desktop, 3500px on mobile) | <img>, <iframe> |
| eager | Loads the resource immediately, regardless of viewport position (default behavior) | <img>, <iframe> |
| 1 | <!-- Eager loading for above-the-fold images --> |
| 2 | <img |
| 3 | src="hero.webp" |
| 4 | alt="Hero banner" |
| 5 | width="1200" |
| 6 | height="600" |
| 7 | loading="eager" |
| 8 | fetchpriority="high" |
| 9 | /> |
| 10 | |
| 11 | <!-- Lazy loading for images below the fold --> |
| 12 | <img |
| 13 | src="photo-1.webp" |
| 14 | alt="Gallery photo 1" |
| 15 | width="800" |
| 16 | height="600" |
| 17 | loading="lazy" |
| 18 | decoding="async" |
| 19 | /> |
| 20 | |
| 21 | <img |
| 22 | src="photo-2.webp" |
| 23 | alt="Gallery photo 2" |
| 24 | width="800" |
| 25 | height="600" |
| 26 | loading="lazy" |
| 27 | decoding="async" |
| 28 | /> |
| 29 | |
| 30 | <!-- Lazy loading for iframes --> |
| 31 | <iframe |
| 32 | src="https://example.com/widget" |
| 33 | title="Widget" |
| 34 | width="400" |
| 35 | height="300" |
| 36 | loading="lazy" |
| 37 | sandbox="allow-scripts" |
| 38 | ></iframe> |
| 39 | |
| 40 | <!-- Picture element with lazy loading --> |
| 41 | <picture> |
| 42 | <source srcset="photo.avif" type="image/avif" /> |
| 43 | <source srcset="photo.webp" type="image/webp" /> |
| 44 | <img |
| 45 | src="photo.jpg" |
| 46 | alt="Landscape" |
| 47 | width="1920" |
| 48 | height="1080" |
| 49 | loading="lazy" |
| 50 | decoding="async" |
| 51 | /> |
| 52 | </picture> |
best practice
The size and depth of the DOM directly impact rendering performance. A large DOM consumes more memory, increases layout computation time, and degrades selector matching. Lighthouse flags pages with more than 1,500 DOM nodes and recommends fewer than 30 levels of nesting.
| 1 | <!-- Bloated DOM — excessive nesting --> |
| 2 | <div class="wrapper"> |
| 3 | <div class="container"> |
| 4 | <div class="inner"> |
| 5 | <div class="content"> |
| 6 | <div class="text"> |
| 7 | <p>Hello World</p> |
| 8 | </div> |
| 9 | </div> |
| 10 | </div> |
| 11 | </div> |
| 12 | </div> |
| 13 | |
| 14 | <!-- Lean DOM — minimal nesting --> |
| 15 | <section> |
| 16 | <p>Hello World</p> |
| 17 | </section> |
| 18 | |
| 19 | <!-- Avoid wrapper divs for CSS — use pseudo-elements --> |
| 20 | <!-- Instead of: --> |
| 21 | <div class="card"> |
| 22 | <div class="card-inner"> |
| 23 | <h2>Title</h2> |
| 24 | </div> |
| 25 | </div> |
| 26 | |
| 27 | <!-- Use: --> |
| 28 | <div class="card"> |
| 29 | <h2>Title</h2> |
| 30 | </div> |
| 31 | <style> |
| 32 | .card::before { |
| 33 | /* replaces card-inner */ |
| 34 | content: ""; |
| 35 | display: block; |
| 36 | } |
| 37 | </style> |
pro tip
A reflow (or layout) occurs when the browser recalculates the position and geometry of elements in the document. Reflows are expensive because they invalidate part or all of the render tree and force the browser to re-layout and re-paint.
| 1 | <!-- Batch DOM operations — bad practice --> |
| 2 | <script> |
| 3 | const list = document.getElementById("list"); |
| 4 | for (const item of items) { |
| 5 | list.innerHTML += '<li>' + item + '</li>'; // reflow per iteration |
| 6 | list.style.color = colors[i]; // another reflow |
| 7 | } |
| 8 | </script> |
| 9 | |
| 10 | <!-- Batch DOM operations — good practice --> |
| 11 | <script> |
| 12 | const fragment = document.createDocumentFragment(); |
| 13 | for (const item of items) { |
| 14 | const li = document.createElement("li"); |
| 15 | li.textContent = item; |
| 16 | fragment.appendChild(li); |
| 17 | } |
| 18 | // Single reflow |
| 19 | document.getElementById("list").appendChild(fragment); |
| 20 | // Single reflow for style change |
| 21 | document.getElementById("list").className = "highlight"; |
| 22 | </script> |
| 23 | |
| 24 | <!-- Use requestAnimationFrame for animations --> |
| 25 | <script> |
| 26 | function animate(el) { |
| 27 | let start = performance.now(); |
| 28 | function frame(now) { |
| 29 | const progress = Math.min((now - start) / 1000, 1); |
| 30 | el.style.transform = 'translateX(' + (progress * 200) + 'px)'; |
| 31 | if (progress < 1) requestAnimationFrame(frame); |
| 32 | } |
| 33 | requestAnimationFrame(frame); |
| 34 | } |
| 35 | </script> |
best practice
Content Security Policy (CSP) is an HTTP security header that helps prevent XSS and data injection attacks. While primarily a security feature, CSP can affect performance by restricting inline styles, scripts, and resource loading. A poorly configured CSP can break pages or block critical resources.
| 1 | <!-- CSP via meta tag --> |
| 2 | <meta |
| 3 | http-equiv="Content-Security-Policy" |
| 4 | content="default-src 'self'; |
| 5 | script-src 'self' https://analytics.example.com; |
| 6 | style-src 'self' 'unsafe-inline'; |
| 7 | img-src 'self' https://images.example.com data:; |
| 8 | font-src 'self' https://fonts.gstatic.com; |
| 9 | connect-src 'self' https://api.example.com; |
| 10 | frame-ancestors 'none'; |
| 11 | base-uri 'self'" |
| 12 | > |
| 13 | |
| 14 | <!-- CSP via HTTP header (preferred): |
| 15 | Content-Security-Policy: |
| 16 | default-src 'self'; |
| 17 | script-src 'self' 'nonce-abc123' https://analytics.example.com; |
| 18 | style-src 'self' 'unsafe-inline'; |
| 19 | img-src 'self' data:; |
| 20 | font-src 'self' https://fonts.gstatic.com; |
| 21 | frame-ancestors 'none'; |
| 22 | base-uri 'self' |
| 23 | |
| 24 | <!-- Nonce-based CSP for inline scripts --> |
| 25 | <script nonce="abc123"> |
| 26 | // This inline script is allowed by the nonce |
| 27 | initApp(); |
| 28 | </script> |
note
Web fonts can significantly impact performance. The browser must download font files before rendering text styled with them, which can delay the First Contentful Paint (FCP) and Largest Contentful Paint (LCP). The font-display CSS property controls how fonts are rendered during loading.
| font-display | Behavior | Use Case |
|---|---|---|
| swap | Renders fallback font immediately, swaps to custom font when loaded | Default for most web fonts — good balance |
| block | Invisible text for ~3s, then swap | Brand-critical typography (use sparingly) |
| fallback | Invisible text for ~100ms, fallback for ~3s, then swap | Compromise between swap and optional |
| optional | Invisible text for ~100ms, then fallback permanently if font hasn't loaded | Slow connections — font never blocks text |
| 1 | <!-- Preload critical font files --> |
| 2 | <link |
| 3 | rel="preload" |
| 4 | href="/fonts/inter-variable.woff2" |
| 5 | as="font" |
| 6 | type="font/woff2" |
| 7 | crossorigin |
| 8 | > |
| 9 | |
| 10 | <!-- Preconnect to font origin --> |
| 11 | <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 12 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 13 | |
| 14 | <!-- CSS with font-display --> |
| 15 | <style> |
| 16 | @font-face { |
| 17 | font-family: "Inter"; |
| 18 | src: url("/fonts/inter-variable.woff2") format("woff2"); |
| 19 | font-display: swap; /* Show fallback text immediately */ |
| 20 | font-weight: 100 900; |
| 21 | font-stretch: 25% 151%; |
| 22 | } |
| 23 | |
| 24 | @font-face { |
| 25 | font-family: "Playfair Display"; |
| 26 | src: url("/fonts/playfair.woff2") format("woff2"); |
| 27 | font-display: optional; /* Don't block text on slow networks */ |
| 28 | } |
| 29 | |
| 30 | body { |
| 31 | font-family: "Inter", system-ui, -apple-system, sans-serif; |
| 32 | } |
| 33 | |
| 34 | h1, h2, h3 { |
| 35 | font-family: "Playfair Display", Georgia, serif; |
| 36 | } |
| 37 | </style> |
| 38 | |
| 39 | <!-- Google Fonts with display=swap --> |
| 40 | <link |
| 41 | href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" |
| 42 | rel="stylesheet" |
| 43 | > |
pro tip
Web performance is measured using standardized metrics that capture different aspects of the user experience. Core Web Vitals are a set of metrics that Google uses to evaluate page experience and influence search rankings.
| Metric | Full Name | Measures | Target |
|---|---|---|---|
| LCP | Largest Contentful Paint | When the largest visible element (image, text block) is rendered | < 2.5s |
| CLS | Cumulative Layout Shift | Unexpected visual movement during page load | < 0.1 |
| INP | Interaction to Next Paint | Responsiveness to user interactions | < 200ms |
| TTFB | Time to First Byte | Time from request to first byte of response | < 800ms |
| FCP | First Contentful Paint | When first text or image is rendered | < 1.8s |
| TBT | Total Blocking Time | Sum of blocking time from long tasks | < 200ms |
| SI | Speed Index | How quickly content is visually displayed | < 3.4s |
info
| 1 | <!-- LCP optimization: preload hero image --> |
| 2 | <link |
| 3 | rel="preload" |
| 4 | href="hero-1920.webp" |
| 5 | as="image" |
| 6 | imagesrcset="hero-640.webp 640w, hero-1024.webp 1024w, hero-1920.webp 1920w" |
| 7 | imagesizes="100vw" |
| 8 | fetchpriority="high" |
| 9 | > |
| 10 | |
| 11 | <!-- CLS prevention: explicit width/height --> |
| 12 | <img |
| 13 | src="hero-1920.webp" |
| 14 | alt="Hero" |
| 15 | width="1920" |
| 16 | height="800" |
| 17 | fetchpriority="high" |
| 18 | decoding="async" |
| 19 | > |
| 20 | |
| 21 | <!-- CLS prevention: CSS aspect-ratio boxes --> |
| 22 | <style> |
| 23 | .video-wrapper { |
| 24 | aspect-ratio: 16 / 9; |
| 25 | width: 100%; |
| 26 | } |
| 27 | .ad-container { |
| 28 | min-height: 250px; |
| 29 | width: 100%; |
| 30 | } |
| 31 | </style> |
| 32 | |
| 33 | <!-- INP optimization: avoid long tasks --> |
| 34 | <script> |
| 35 | // Break up long tasks with yield |
| 36 | async function processItems(items) { |
| 37 | for (const item of items) { |
| 38 | await new Promise(r => setTimeout(r, 0)); |
| 39 | processItem(item); |
| 40 | } |
| 41 | } |
| 42 | </script> |
Chrome DevTools and Lighthouse provide powerful tools for analyzing and debugging HTML performance. Use the Performance panel to record runtime performance, the Network panel to inspect resource loading, and Lighthouse to get actionable audit scores.
| 1 | # Run Lighthouse from CLI |
| 2 | npx lighthouse https://example.com --view |
| 3 | |
| 4 | # Run Lighthouse as CI |
| 5 | npx lhci collect --url=https://example.com |
| 6 | npx lhci assert |
| 7 | |
| 8 | # Use WebPageTest for detailed analysis |
| 9 | # https://www.webpagetest.org |
| 10 | |
| 11 | # Check Core Web Vitals from CrUX API |
| 12 | curl "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$API_KEY" \ |
| 13 | -H "Content-Type: application/json" \ |
| 14 | -d '{"origin": "https://example.com"}' |
pro tip