How to Fix Slow LCP When the H1 (or Any Text Block) Is the LCP Element

This guide applies the phase model from Measuring LCP with Chrome DevTools to a case that confuses many teams, within the wider Core Web Vitals & Measurement section: the LCP element is not an image at all, but a heading or a block of text.

Text LCP is common on articles, documentation, landing pages with typographic heroes, and any mobile layout where the hero image is hidden or pushed below the fold. It is often assumed to be "free" — text has no download — yet pages with a text LCP regularly report 3s or more at p75 on mobile. The reason is that a text element cannot paint until three things are true: the HTML containing it has arrived, every render-blocking stylesheet has loaded, and either its web font has loaded or the font-display policy allows a fallback to paint. If the text is rendered by JavaScript, add the time to download, parse and execute that too.

What a text LCP element waits for The chain of dependencies that must resolve before a heading can paint as the LCP element. What a text LCP element waits for HTML arrives TTFB + document Blocking CSS every stylesheet in head Web font or fallback per font-display JS render only if client-rendered Text paints LCP recorded Text has no resource load phase of its own, so its LCP is almost entirely TTFB plus render delay.

Rapid Diagnosis

Work through this checklist in DevTools with "Fast 4G" and 4x CPU throttling enabled.

  • Confirm the element. In the Performance panel, click the LCP marker in the Timings track; the summary shows the node. Hover it to highlight. If it is an h1, p or div containing text, this guide applies.
  • Read the LCP breakdown. The "LCP by phase" insight shows TTFB and render delay; for text there is no load delay or load time. If render delay is more than half of LCP, the problem is between HTML arrival and paint.
  • Inspect the network waterfall before the LCP marker. Every stylesheet that finishes before the marker and has "Highest" priority is a candidate blocker. Note any CSS file larger than ~50KB.
  • Check the Fonts. In the Network panel, filter by "Font". If the heading's font file finishes just before the LCP marker, the paint waited for it.
  • View source. If the heading text is not in the initial HTML response (search for it in the Response tab of the document request), the page is client-rendered and JavaScript is on the critical path.

Root Cause Analysis

1. Render-blocking CSS on a slow connection. Every <link rel="stylesheet"> in the head blocks first render. A 180KB framework stylesheet over a mobile connection takes several hundred milliseconds even from a CDN, and the heading cannot paint until it finishes — even if only 4KB of it styles the heading.

2. Web font blocking with font-display: block or the default auto. Many font setups hide text for up to three seconds while the font downloads ("flash of invisible text"). The heading is in the DOM and laid out, but invisible text does not count as painted. LCP is recorded when the font arrives — or when the block period ends.

3. Client-side rendering of the heading. In single-page apps without server rendering, the HTML is an empty shell. The heading only exists after the JavaScript bundle downloads, parses, executes and fetches data. Render delay then includes the entire bundle cost.

4. Late-discovered CSS. Stylesheets loaded via @import inside another stylesheet, or injected by JavaScript, are discovered late and form a waterfall: CSS A must download before the browser learns about CSS B.

Text LCP breakdown on a slow article page Bar chart of the phases of a 3.4 second text LCP, with blocking CSS and the web font taking most of the render delay. Text LCP breakdown on a slow article page TTFB 620ms Blocking CSS (2 files) 1150ms Web font (block period) 1240ms Layout + paint 390ms

Step-by-Step Resolution

1. Inline critical CSS and defer the rest

Extract the CSS needed for above-the-fold content, inline it in the head, and load the full stylesheet without blocking render. The heading can then paint as soon as the HTML arrives.

html
<head>
  <style>/* ~8-14KB of critical rules: layout shell, header, h1, hero text */</style>
  <link rel="preload" href="/css/site.4f2a.css" as="style">
  <link rel="stylesheet" href="/css/site.4f2a.css" media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/css/site.4f2a.css"></noscript>
</head>
<!-- trade-off: inlined CSS is re-downloaded with every HTML response and
     cannot be cached separately. Keep it under ~14KB (one TCP round trip's
     worth of compressed bytes) and only for templates where text is LCP. -->

Expected outcome: render delay falls by the stylesheet download time, typically 300–900ms on mobile. The full technique, including build tooling, is in extracting critical CSS at build time.

2. Let the heading paint in a fallback font

Use font-display: swap (or optional for body text where a swap would be jarring) so the heading paints immediately in a fallback face, and pair it with metric overrides so the swap does not shift layout.

css
@font-face {
  font-family: "Brand Serif";
  src: url("/fonts/brand-serif-700.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
@font-face {                       /* metric-matched fallback: no shift on swap */
  font-family: "Brand Serif Fallback";
  src: local("Georgia");
  size-adjust: 104%;
  ascent-override: 92%;
}
h1 { font-family: "Brand Serif", "Brand Serif Fallback", serif; }
/* trade-off: with swap, LCP is recorded at the FALLBACK paint, then possibly
   again if the swapped text is larger. Matching metrics keeps the second
   candidate the same size so it does not replace the first. */

Expected outcome: LCP is recorded at the first fallback paint; on slow connections this removes 500ms–2s. See font-display swap vs optional for LCP for when each is the better choice.

3. Preload the one font the heading uses

If you want the real font to win the race rather than swap in later, preload exactly the weight and style used by the LCP heading — not the whole family.

html
<link rel="preload" href="/fonts/brand-serif-700.woff2" as="font" type="font/woff2" crossorigin>
<!-- trade-off: preloaded fonts compete for bandwidth with CSS and the LCP
     image (if any). Preload one, at most two, font files. Preloading four
     weights usually makes LCP worse, not better. -->

Expected outcome: the font arrives in parallel with CSS rather than after it, often allowing the first paint to use the real face at no LCP cost. The crossorigin attribute is required even for same-origin fonts — preloading web fonts with crossorigin explains why.

4. Server-render the heading

If the heading is client-rendered, no CSS or font fix will help as much as putting it in the HTML. Server-side render or statically generate at least the above-the-fold content.

javascript
// Next.js App Router: a server component renders the heading into the HTML.
export default async function Article({ params }) {
  const post = await getPost(params.slug);          // runs on the server
  return <h1 className="article-title">{post.title}</h1>;
}
// trade-off: server rendering moves work to TTFB. If getPost() is slow, TTFB
// grows and LCP may not improve. Cache the data or stream the shell first.

Expected outcome: render delay drops by the full JavaScript cost — commonly 1–2s on mid-tier mobile for SPA shells.

Before and after on a typical article template Comparison of a slow text LCP setup with blocking CSS and FOIT against an optimised setup with inline critical CSS, swap with metric overrides and a server-rendered heading. Before and after on a typical article template Before: 3.4s LCP • Two blocking stylesheets, 210KB • font-display auto (invisible text) • Heading rendered after hydration After: 1.6s LCP • 12KB inline critical CSS, rest async • swap with metric-matched fallback • Heading in server HTML

Verification

Record the page again with the same throttling. The LCP marker should now sit close to FCP — for text LCP they are often identical, because the first contentful paint is the heading. Check that render delay is now a small fraction of LCP and that no stylesheet or font finishes immediately before the marker.

Lock it in with Lighthouse CI so a new blocking stylesheet does not quietly return:

javascript
// lighthouserc.js
module.exports = {
  ci: {
    assert: {
      assertions: {
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'render-blocking-resources': ['warn', { maxLength: 0 }],
        'font-display': 'error',
      },
    },
  },
};
// trade-off: maxLength 0 on render-blocking resources is strict — one small
// blocking stylesheet can be acceptable. Use 'warn' so it informs rather than
// blocks, and rely on the LCP assertion as the hard gate.

In the field, confirm the LCP element type in your RUM (attribution.lcpEntry.element.tagName from the web-vitals attribution build) and compare p75 for text-LCP page views before and after the release.

Edge Cases Specific to Text LCP

The LCP element changes after the font swap. If the web font is wider than the fallback, a heading that wraps onto three lines in the real font but two in the fallback becomes a larger element after swap and generates a new, later LCP candidate. Metric-matched fallbacks prevent this; without them, swap can make LCP later than expected.

Text inside a hidden-then-revealed container. Hero headings that fade in with an opacity: 0 → 1 animation do not count as painted while invisible, and Chromium ignores opacity-zero elements as candidates until they become visible. A 600ms entrance animation adds 600ms to LCP. Start the animation from a visible state or drop it from the LCP element.

A cookie banner becomes the LCP. On small screens, a consent banner's paragraph can be larger than the heading. It then becomes the LCP element, and its timing — often injected by a third-party script — defines your metric. Server-render the banner markup or constrain its text size; see fixing CLS from cookie consent banners for the layout side of the same problem.

FAQ

Is text LCP always faster than image LCP?

Not inherently. Text skips the resource download, but it depends on CSS and fonts that images often do not. A page with an optimised, preloaded hero image and a lean stylesheet can beat a text-LCP page that blocks on 200KB of CSS and two font files. The advantage of text LCP is that it is entirely within your control — no image pipeline to tune.

Why does my LCP equal FCP after the fix?

Because the first content the browser painted was also the largest. For text-hero pages that is the ideal outcome: there is nothing left to wait for after the first paint. If LCP still lags FCP, something larger painted later — usually an image that loaded after the heading, or a heading whose font swapped to a larger size.

Should I use system fonts for headings to fix LCP?

It is the most reliable fix and worth considering for body text, but it is a design decision, not just a performance one. With a well-matched fallback and swap, a custom heading font costs almost nothing in LCP. Use system fonts where the brand allows; otherwise get the fallback metrics right.