Performance
LCP under 1.8 seconds on mobile, and the five things that actually cause a content site to miss it. Inlined CSS, self-hosted fonts, modern image formats, code splitting and static-first rendering.
5 min read · updated 2026-07-26· last reviewed 2026-07-26
The five causes
The principle
On a content-heavy site, a failed mobile LCP almost always traces to one of five things. Knowing the list turns performance work from an open-ended optimisation exercise into a checklist.
- A render-blocking stylesheet.
- An external font request on the critical path.
- An oversized image in the hero.
- A client-side data fetch the first paint depends on.
- JavaScript that has to execute before content appears.
Everything else is rounding error until those five are handled.
Render-blocking CSS
The principle
An external stylesheet is a round trip that must complete before anything paints. On a cold mobile connection that is a DNS lookup, a connection, and a transfer, all before the first character appears.
Inlining the route's CSS into the document removes the request entirely. The cost is that the CSS is no longer cached across navigations, which for a site with distinct per-route styling is a good trade and for an app with one large shared stylesheet is not.
The implementation
// next.config.ts
experimental: {
inlineCss: true,
},curl -s https://example.com/ | grep -c '<link rel="stylesheet"'Zero is the target. A <style> block in the head is what should be there instead.
Fonts
The principle
A font loaded from a third-party host costs a DNS lookup, a TLS handshake and a render-blocking request, all before a single character is drawn. Self-hosting removes all three.
display: swap means text renders immediately in a fallback and swaps when the webfont arrives. The alternative, block, produces invisible text for up to three seconds, which is a failed LCP by definition if your LCP element is text.
The implementation
const inter = Inter({
subsets: ["latin"], // subset, or you ship every script
display: "swap",
variable: "--font-inter",
weight: ["400", "500", "600", "700"],
});curl -s https://example.com/ | grep -E 'fonts.googleapis|fonts.gstatic'Empty output is the target. A single leftover <link> reintroduces everything the build removed.
Pick fallbacks with similar metrics, or the swap causes a visible reflow that shows up as CLS.
Images
The principle
AVIF is typically half the bytes of a comparable JPEG at the same perceived quality. WebP is the fallback. Both are supported broadly enough that serving a JPEG as the primary format is now a choice rather than a constraint.
The larger win is sizing. Serving a 1600px-wide image into a 375px viewport is the most common single cause of a failed mobile LCP, and it is invisible on the desktop where it was built.
The implementation
images: {
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 2678400,
deviceSizes: [360, 414, 640, 750, 828, 1080, 1200, 1920],
},deviceSizes should start low. A default list beginning at 640 means a 375px phone downloads a 640px image, which is 3x the pixels it can display at 2x density.
Code splitting and animation
The principle
An exit-intent modal that ships in the main bundle costs every visitor the bytes for a component most of them never see. The same applies to validators, charts and anything below the fold that needs JavaScript.
Animation libraries deserve specific mention because the full import is large enough on its own to cost a page its performance score.
The implementation
const ExitIntentModal = dynamic(() => import("@/components/marketing/exit-intent-modal"), {
ssr: false,
});grep -rn 'from "framer-motion"' components app | grep -v '\bm\b'Use the minimal namespace on anything that can appear on a marketing page. The full namespace is fine inside an authenticated app route where the bundle is already loaded and the page is not being measured.
Static-first, and why it is a business requirement
The principle
If the sales page needs the database to render, a database outage stops revenue rather than degrading a feature.
Prerender every marketing page and every programmatic page. The authenticated app can be dynamic, because a signed-in user with a database outage has a broken app either way, and that is a much smaller population than everyone who might buy.
Verifying it
Stop the database and request the landing page and the pricing page. Both should return 200 with full content. If either fails, the funnel is coupled to the app, and the first outage will demonstrate it.
Layout shift
The principle
Three elements cause nearly all CLS on a marketing site, because all three appear after first paint by design: the sticky call to action, the cookie banner, and web fonts.
The fixes are mechanical. Fixed-position elements do not shift layout. Reserve space for anything that will occupy it. Match fallback font metrics so the swap does not reflow.
Checks this chapter covers
Each one has a command you can run against your own site.
- →LCP under 1.8 seconds on mobile for every templateperf-lcp-under-1800
- →Route CSS is inlined so it does not block first paintperf-no-render-blocking-css
- →Fonts are self-hosted with display swap and subsetting, and zero external font requestsperf-fonts-self-hosted
- →Images served as AVIF or WebP with correct sizes and a long cache TTLperf-images-modern-formats
- →Non-critical interactive components are code-splitperf-code-split-noncritical
- →Animation libraries are imported through their minimal namespace on marketing pagesperf-animation-namespace
- →Static assets carry a one-year immutable cache header, with versioned URLsperf-immutable-assets
- →No layout shift from the sticky CTA, the cookie banner or web fontsperf-no-cls-from-chrome
- →Every marketing and programmatic page is prerendered, so a database outage cannot take down the funnelperf-static-first