|$ curl https://forge-ai.dev/api/markdown?path=docs/html/performance
$cat docs/html-performance.md
updated This week·35 min read·published

HTML Performance

HTMLPerformanceAdvancedAdvanced
Introduction

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 HTML element is the first thing the browser encounters after the doctype. Keep your markup lean, use semantic elements, and avoid unnecessary nesting. Every extra node in the DOM tree consumes memory and increases rendering cost.
Critical Rendering Path

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.

1DOM (Document Object Model) — The browser parses HTML and constructs a tree of nodes representing the document structure.
2CSSOM (CSS Object Model) — CSS is parsed into a separate tree of style rules that cascade and apply to DOM nodes.
3Render Tree — The DOM and CSSOM are combined into a render tree containing only visible elements with computed styles.
4Layout (Reflow) — The browser calculates the geometry of each visible element: position, size, and spacing.
5Paint — The browser converts the render tree into pixels on the screen, layer by layer.

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.

critical-rendering-path.html
HTML
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

The single biggest CRP win: inline critical CSS in the <head> and load non-critical CSS asynchronously. This eliminates a render-blocking request and typically improves First Contentful Paint (FCP) by 200-500ms on slow networks.
Resource Loading Hints

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.

HintSyntaxBehavior
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.
resource-hints.html
HTML
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

Overusing preload can hurt performance. Preload competes for bandwidth and network connections. Only preload resources that are critical to the current page and would be discovered late by the parser. A good rule of thumb: preload the LCP image, hero fonts, and above-the-fold CSS.
Async vs Defer Script Loading

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.

AttributeFetch TimingExecution TimingOrderUse Case
(none)At parse positionImmediately after fetchIn document orderLegacy — blocks parser
asyncIn parallel with parsingAs soon as fetchedUnorderedIndependent scripts (analytics, widgets)
deferIn parallel with parsingAfter parsing completesIn document orderScripts that need DOM access
async-defer.html
HTML
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

Use defer for your application scripts and async for third-party scripts. Deferred scripts execute in order after DOM parsing, while async scripts execute whenever they finish downloading. Never use both — async takes precedence over defer.
Lazy Loading

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 ValueBehaviorSupported Elements
lazyDefers loading until the element approaches the viewport (typically within 1250px on desktop, 3500px on mobile)<img>, <iframe>
eagerLoads the resource immediately, regardless of viewport position (default behavior)<img>, <iframe>
lazy-loading.html
HTML
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

Always include width and height attributes on lazy-loaded images. This lets the browser calculate the aspect ratio and reserve space before the image loads, preventing Cumulative Layout Shift (CLS). Without dimensions, lazy images cause a layout shift when they load.
Reducing DOM Size

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.

Aim for fewer than 1,500 total DOM nodes
Keep maximum DOM depth under 32 levels
Avoid parent elements that only wrap children for styling — use CSS pseudo-elements instead
Use semantic elements to reduce unnecessary <div> wrappers
Virtualize long lists with libraries like TanStack Virtual or react-window
dom-size.html
HTML
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

Use the document.querySelectorAll('*').length command in the browser console to count DOM nodes. For large pages with dynamic content, consider virtualization. Also check performance.memory.usedJSHeapSize to monitor memory usage from DOM nodes.
Minimizing Reflows

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.

Batch DOM read/write operations — read first, then write (use document.createDocumentFragment() or a library)
Use classList.add() instead of manipulating style properties individually
Avoid reading layout properties (offsetHeight, clientWidth, getComputedStyle) during DOM updates
Use position: absolute or position: fixed for animated elements to isolate them from the layout flow
Use will-change: transform to promote elements to their own compositor layer
Prefer CSS transform and opacity animations — they trigger compositing only, not layout
minimizing-reflows.html
HTML
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

The most impactful reflow optimization is minimizing layout-triggering property accesses (offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollLeft, clientTop, clientWidth, getComputedStyle) inside loops. Read all needed values first, then perform all writes in a batch.
Content Security (CSP)

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.

csp.html
HTML
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):
15Content-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

For performance-critical pages, avoid 'unsafe-inline' in script-src if possible. Use nonce or hash based allowlisting instead. This lets you block inline scripts while allowing trusted ones, improving both security and performance by preventing script injection.
Font Loading

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-displayBehaviorUse Case
swapRenders fallback font immediately, swaps to custom font when loadedDefault for most web fonts — good balance
blockInvisible text for ~3s, then swapBrand-critical typography (use sparingly)
fallbackInvisible text for ~100ms, fallback for ~3s, then swapCompromise between swap and optional
optionalInvisible text for ~100ms, then fallback permanently if font hasn't loadedSlow connections — font never blocks text
font-loading.html
HTML
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

Use font-display: optional for non-critical decorative fonts. On slow connections (3G), the font will not load and the fallback is used permanently. This prevents font loading from delaying text rendering. Always pair custom fonts with well-chosen fallback fonts that match their metrics.
Performance Metrics

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.

MetricFull NameMeasuresTarget
LCPLargest Contentful PaintWhen the largest visible element (image, text block) is rendered< 2.5s
CLSCumulative Layout ShiftUnexpected visual movement during page load< 0.1
INPInteraction to Next PaintResponsiveness to user interactions< 200ms
TTFBTime to First ByteTime from request to first byte of response< 800ms
FCPFirst Contentful PaintWhen first text or image is rendered< 1.8s
TBTTotal Blocking TimeSum of blocking time from long tasks< 200ms
SISpeed IndexHow quickly content is visually displayed< 3.4s

info

LCP is the most important performance metric for HTML optimization. The LCP candidate is typically an <img> or a text block. Optimize it by: preloading the hero image, using responsive srcset, setting explicit dimensions, and avoiding render-blocking resources above it.
core-web-vitals.html
HTML
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>
Lighthouse & DevTools

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.

Performance panel — record page load and inspect the flame chart for long tasks, layout thrashing, and paint events
Network panel — check resource waterfall, identify blocking requests, verify preload/prefetch headers
Lighthouse — run audits for Performance, Accessibility, Best Practices, SEO, and PWA
Coverage tab — identify unused CSS and JavaScript to eliminate dead code
Memory tab — take heap snapshots to detect DOM node leaks and memory growth
Rendering tab — enable paint flashing, layout shift regions, and layer borders for debugging
lighthouse-cli.html
Bash
1# Run Lighthouse from CLI
2npx lighthouse https://example.com --view
3
4# Run Lighthouse as CI
5npx lhci collect --url=https://example.com
6npx lhci assert
7
8# Use WebPageTest for detailed analysis
9# https://www.webpagetest.org
10
11# Check Core Web Vitals from CrUX API
12curl "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$API_KEY" \
13 -H "Content-Type: application/json" \
14 -d '{"origin": "https://example.com"}'
🔥

pro tip

Run Lighthouse with simulated throttling (Slow 4G, 4x CPU slowdown) to get realistic field performance scores. Use the "View Trace" button after a Lighthouse run to dive deeper into the Performance panel with automatic annotations for bottlenecks.
Best Practices
Inline critical CSS in <head> and load non-critical CSS asynchronously to eliminate render-blocking stylesheets
Preload above-the-fold resources (LCP image, hero fonts, critical scripts) using rel=preload
Use defer for application scripts and async for third-party scripts — never use synchronous scripts in <head>
Add loading=lazy to all images and iframes below the fold, with explicit width and height attributes to prevent CLS
Serve images in modern formats (AVIF, WebP) with responsive srcset and sizes attributes
Keep DOM size under 1,500 nodes with fewer than 32 levels of nesting
Use font-display: swap or optional to prevent invisible text during font loading
Preconnect to third-party origins (analytics, CDNs, font providers) to eliminate connection latency
Set fetchpriority=high on the LCP element and fetchpriority=low on below-the-fold resources
Aim for TTFB under 800ms, LCP under 2.5s, CLS under 0.1, and INP under 200ms
Minimize reflows by batching DOM operations and using CSS transforms for animations
Use the Performance panel and Lighthouse regularly — measure, optimize, repeat
$Blueprint — Engineering Documentation·Section ID: HTML-37·Revision: 1.0