Loading performance in markup

HTML · Expert · 14 min

Largest Contentful Paint and layout shift are mostly decided by how resources are declared in HTML.

The browser turns HTML into pixels through the critical rendering path: parse HTML into the DOM, fetch and parse CSS into the CSSOM (render-blocking), run blocking scripts, then lay out and paint. Your markup decides what lands on that path.

Priorities. Browsers assign fetch priorities heuristically: CSS and blocking scripts high, images low until they're found to be in the viewport. fetchpriority="high" on the hero image tells the browser up front that it's the Largest Contentful Paint element; fetchpriority="low" demotes, say, off-screen carousel slides. It works on <img>, <link rel="preload">, <script> and <iframe>.

Discoverability. The preload scanner only finds resources written in HTML. A hero image set as a CSS background or injected by JavaScript is discovered late. Put the LCP image in an <img> in the initial HTML, or preload it.

Lazy loading. loading="lazy" on images and iframes below the fold saves bandwidth, but on the LCP image it's harmful: it waits for layout to know the image is visible.

Layout stability. width/height on media, and reserved space for ads and embeds, prevent Cumulative Layout Shift.

Other hints:

  • decoding="async" lets image decoding happen off the critical path.
  • <link rel="preconnect"> warms up connections to third-party origins.
  • Speculation rules (<script type="speculationrules">) let Chromium prefetch or prerender likely next pages.

Measure, don't guess: Lighthouse and the browser's performance panel show which request was the LCP element and what delayed it.

Example

<head>
  <link rel="stylesheet" href="/css/critical.css">          <!-- render-blocking -->
  <link rel="preconnect" href="https://img.cdn.example" crossorigin>
  <script type="speculationrules">
    { "prefetch": [{ "where": { "href_matches": "/docs/*" }, "eagerness": "moderate" }] }
  </script>
</head>
<body>
  <!-- LCP image: in HTML, eager, high priority, sized -->
  <img src="https://img.cdn.example/hero.avif" alt="Product dashboard"
       width="1600" height="900" fetchpriority="high">

  <!-- Below the fold: lazy, async decode -->
  <img src="/img/feature-3.webp" alt="Export to PDF" width="800" height="500"
       loading="lazy" decoding="async">

  <iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="Demo video"
          width="560" height="315" loading="lazy"></iframe>
</body>

Remember

  • Put the LCP image in the HTML with fetchpriority="high" and never lazy
  • The preload scanner can only find resources in markup
  • Sizes on media prevent layout shift
  • Measure LCP and CLS before and after changes

Watch out for

  • Setting the hero image as a CSS background, hiding it from the preload scanner.

Check yourself

  1. Which change most likely improves LCP when the hero is <img loading="lazy" src="hero.jpg">?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path

Builds on

Unlocks

Nothing builds on this yet.