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.