SEO for Solos
Chapter 13Free

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.

  1. A render-blocking stylesheet.
  2. An external font request on the critical path.
  3. An oversized image in the hero.
  4. A client-side data fetch the first paint depends on.
  5. 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

typescript
// next.config.ts
experimental: {
  inlineCss: true,
},
bash
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

typescript
const inter = Inter({
  subsets: ["latin"],       // subset, or you ship every script
  display: "swap",
  variable: "--font-inter",
  weight: ["400", "500", "600", "700"],
});
bash
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

typescript
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

typescript
const ExitIntentModal = dynamic(() => import("@/components/marketing/exit-intent-modal"), {
  ssr: false,
});
bash
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.

Product analytics are optional. On the skill page, X advertising measurement is also optional and shares your visit and ad identifiers with X. Both are off until you allow them. Privacy policy.