Runtime Loading Performance

This section documents general HTML5 and CSS concepts — it is not tied to any specific framework or library. This content was generated with the assistance of AI. Verify it against current MDN documentation and browser-support tables (caniuse.com) before relying on it in production, since HTML/CSS features and browser support continue to evolve.

Some performance work happens as part of a build (minification, bundling, image compression — covered in Build-Time Performance Optimization). This page covers the other half: how a browser loads and paints a page at runtime, and the HTML/CSS/JavaScript techniques that control it — inlining critical CSS while deferring the rest, deferring or lazily creating <script> tags, native image lazy loading, and choosing the right image format, size, and loading priority. It starts with the vocabulary and tooling used to measure whether any of this is actually working: Core Web Vitals and the Lighthouse auditing tool.

Core Web Vitals and measuring performance

Web Vitals is an initiative from Google that standardizes how the user-perceived performance of a page is measured. The subset it considers most important — the Core Web Vitals — currently covers loading speed, visual stability, and responsiveness. Each metric has a "good", "needs improvement", and "poor" band, and getting these right improves both the experience for users and, since Google factors them into ranking, SEO.

Largest Contentful Paint (LCP)

The time taken for the largest element in the viewport — typically a hero image, a background image, or a large block of text — to render. It approximates when a user perceives the main content as having arrived.

  • Good: 2.5 seconds or less.

  • Needs improvement: between 2.5 and 4 seconds.

  • Poor: over 4 seconds.

Cumulative Layout Shift (CLS)

A score for how much visible content unexpectedly moves around as the page loads, calculated by multiplying the impact fraction (how much of the viewport is affected by a shift) by the distance fraction (how far the affected elements moved). Shifts caused directly by user interaction do not count against the score.

  • Good: below 0.1.

  • Needs improvement: between 0.1 and 0.25.

  • Poor: above 0.25.

Interaction to Next Paint (INP)

A measurement of how quickly a page responds to user interactions — clicks, taps, and key presses — sampled across the entire lifetime of the page rather than just the first interaction. INP replaced First Input Delay (FID) as a Core Web Vital in March 2024, because observing interactivity throughout a session is a better proxy for perceived responsiveness than only measuring the very first interaction. General guidance treats an INP of 200 ms or less as good, over 500 ms as poor, and anything in between as needing improvement.

First Contentful Paint (FCP)

The time from navigation until the first piece of content (text, image, or other DOM element) is painted — an early signal of whether a page feels like it is loading at all, rather than stalled. FCP is not itself one of the three Core Web Vitals above, but Lighthouse reports it alongside them because it is driven by many of the same factors (render-blocking CSS/JavaScript, server response time, network conditions).

  • Good: 1.8 seconds or less.

  • Needs improvement: between 1.8 and 3.0 seconds.

  • Poor: over 3.0 seconds.

Lighthouse

Lighthouse is an auditing tool, built into Chrome DevTools, that runs a page load and scores it across several categories (Performance, Accessibility, Best Practices, SEO, and Progressive Web App). Its Performance report has three parts:

  • Score — an overall percentage, color-coded red (poor)/orange (needs improvement)/green (good), computed as a weighted average of the underlying metrics.

  • Metrics — the individual measurements, including the Core Web Vitals above plus supporting figures such as Total Blocking Time and Speed Index, each independently color-coded.

  • Diagnostics — concrete pointers to what is hurting the score on this page, such as which element caused a layout shift or which images are unoptimized.

To run an audit: open the page in Chrome, open DevTools, select the Lighthouse tab, select just the Performance category, and click Analyze page load. Running it in an incognito window is recommended, since installed extensions can otherwise skew the result. The rest of this page works through the techniques Lighthouse’s diagnostics most commonly flag, and how to address them.

Inlining critical CSS and deferring the rest

An external stylesheet linked with <link rel="stylesheet">, and any inline <style> block, blocks rendering: the browser will not paint anything until it has finished downloading and parsing that CSS. This is fine for the styles needed to render what is visible without scrolling (the "above-the-fold" content) — the browser needs those before it can paint anything useful anyway — but any CSS that only affects content further down the page is pure, avoidable delay to FCP and LCP.

The fix is to split the two apart: inline the critical CSS (the minimal rules needed for above-the-fold content) directly into a <style> block in <head>, so it arrives with the HTML and needs no extra round trip, and load everything else without blocking the initial render.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Optimized Page</title>
  <style>
    /* Critical CSS: only what's needed above the fold */
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
    }
    header {
      background-color: #333;
      color: #fff;
      padding: 1em;
      text-align: center;
    }
  </style>
</head>
<body>
  <header>
    <h1>Welcome to My Website</h1>
  </header>
  <main>
    <!-- Main content -->
  </main>
</body>
</html>

Non-critical CSS is then loaded without blocking rendering, by disguising the stylesheet as a preloaded resource and only switching its rel to stylesheet once it has arrived:

<link
  rel="preload"
  href="styles/non-critical.css"
  as="style"
  onload="this.rel='stylesheet'"
>
<noscript>
  <link rel="stylesheet" href="styles/non-critical.css">
</noscript>

The browser fetches the file as a plain preload (which does not block rendering) and, once it finishes loading, the inline onload handler flips rel to stylesheet, activating the styles. The <noscript> fallback exists because that onload handler is JavaScript — with scripting disabled, the preload would otherwise never turn into an active stylesheet, so <noscript> supplies a plain, blocking <link rel="stylesheet"> instead.

Deferring and lazy-loading JavaScript

A <script> tag with neither async nor defer blocks HTML parsing at the point it appears: the browser stops building the DOM, fetches the script, and executes it before continuing. Two attributes let a script opt out of that:

async

Downloads the script in the background without blocking parsing, then executes it as soon as it arrives — which can be before or after the HTML has finished parsing, and in whatever order scripts happen to finish downloading. Suited to independent scripts that do not depend on the DOM being complete or on running in a particular order (analytics tags are a common example).

<script src="path/to/non-essential-script.js" async></script>
defer

Also downloads in the background without blocking parsing, but always executes after the HTML has finished parsing, in the order the defer scripts appear in the document. Suited to scripts that need the full DOM to be available, or that depend on each other’s execution order.

<script src="path/to/script1.js" defer></script>
<script src="path/to/script2.js" defer></script>

Beyond async/defer, a script can be created and inserted on demand, so it is not even requested until it is actually needed — for example, once the page has settled after DOMContentLoaded:

document.addEventListener('DOMContentLoaded', function () {
  var lazyScript = document.createElement('script');
  lazyScript.src = 'path/to/non-critical-script.js';
  document.body.appendChild(lazyScript);
});

or in direct response to a user action, so the script is only ever fetched by users who actually trigger the feature it powers:

document.getElementById('loadScriptButton').addEventListener('click', function () {
  var script = document.createElement('script');
  script.src = 'path/to/conditional-script.js';
  document.body.appendChild(script);
});

Native lazy loading for images

The loading attribute on <img> tells the browser whether to fetch an image immediately or only once it is about to enter the viewport:

  • loading="eager" (the default) starts the fetch as soon as the browser parses the tag, regardless of whether the image is currently visible. Use it for images that matter to the initial render — most obviously the image that drives your LCP.

  • loading="lazy" postpones the fetch until the browser expects the image to scroll into view soon, so images far down the page no longer compete with above-the-fold content for bandwidth during initial load.

<img src="assets/hero.jpg" alt="" loading="eager">

<!-- further down the page -->
<img src="assets/secondary-image.jpg" alt="" loading="lazy">

A common pattern for a page with one dominant hero image and many secondary images (a gallery, a product grid) is to mark only the hero as eager and everything else as lazy — combined, as covered next, with fetchpriority hints that tell the browser which of the eagerly-loaded images matters most. <img>’s full attribute set, including `srcset/sizes, is documented in Appendix: HTML Element Reference.

Choosing image formats, sizes, and load priority

Images are typically the heaviest assets on a page, so getting their format, size, and priority right has an outsized effect on LCP and CLS.

Serving the right size: srcset and sizes

srcset lists several versions of the same image at different intrinsic widths, and sizes tells the browser how wide the image will actually render at different viewport widths — together they let the browser pick the smallest file that will still look sharp, instead of always downloading a single, one-size-fits-all image.

<img
  src="photo-800.jpg"
  srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Mountain landscape at sunset"
>

Serving the right format: picture and source

<picture> wraps one or more <source> elements plus a fallback <img>, and lets the browser negotiate which format to download based on what it supports — serving a smaller AVIF or WebP file to browsers that can decode it, and falling back to universally-supported JPEG or PNG otherwise. The browser evaluates the <source> elements in order and uses the first one whose type it supports, falling back to the <img> if none match.

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="Description of the image" loading="lazy" width="600" height="400">
</picture>

As a rule of thumb: JPEG suits photographic content, PNG suits images needing transparency or few colors, and WebP/AVIF generally beat both on compression for equivalent quality when the browser supports them — which is exactly the case <picture>/<source> lets you handle without dropping support for browsers that do not. The full <picture>/<source> attribute set is documented in Appendix: HTML Element Reference.

Reserving space: width and height

An image with no declared dimensions occupies zero space until it finishes loading, and then shoves everything below it down the page the moment it arrives — a textbook cause of a poor CLS score. Declaring width and height (in pixels, matching the image’s intrinsic aspect ratio) lets the browser reserve the correct box in the layout before the image has loaded, so nothing shifts once it does:

<img src="image.jpg" alt="Description of the image" width="800" height="600">

Signaling priority: fetchpriority

fetchpriority hints to the browser how urgently a resource should be fetched relative to everything else competing for bandwidth, independently of where it sits in the document. It takes auto (the default, browser-decided), high, or low, and applies to <img> as well as to <link>-referenced resources:

<img src="assets/hero.jpg" alt="" fetchpriority="high">
<img src="assets/thumbnail-01.jpg" alt="" loading="lazy" fetchpriority="low">

A typical pairing is fetchpriority="high" on the single image driving LCP (often combined with loading="eager", since a lazy-loaded LCP image is a contradiction in terms) and fetchpriority="low" on lower-value images that also carry loading="lazy".

Resource hints: preload, prefetch, and preconnect

<link>’s `rel attribute also accepts three hints that reorder or accelerate resource loading ahead of when the browser would otherwise discover the need for them:

  • rel="preload" — fetch a resource now because it is needed for the current page’s initial render (a hero image, a critical font, the non-critical stylesheet pattern shown earlier), without waiting for the browser to encounter it naturally while parsing.

  • rel="prefetch" — fetch a resource at low priority because it will likely be needed on the next page the user navigates to.

  • rel="preconnect" — open the connection (DNS, TCP, TLS) to a server ahead of time, so that when a resource is actually requested from it, the connection setup cost has already been paid.

preload and prefetch take an as attribute describing the resource type (image, style, script, font, fetch, and others), which lets the browser apply the right request priority and content-type matching:

<link rel="preload" as="image" href="assets/hero.jpg">
<link rel="preconnect" href="https://fonts.example.com">
<link rel="prefetch" as="script" href="next-page-bundle.js">

Used together with the critical-CSS split and the lazy-loading/priority attributes above, these hints let a page front-load exactly the handful of assets its LCP and FCP scores actually depend on, while pushing everything else out of the critical path.