A Practical SEO Guide, Part 4: Performance Is SEO

Google made Core Web Vitals an explicit ranking signal, but the ranking factor almost undersells it: a slow site loses the visitor before the content even paints. You can't rank for an audience that already bounced. So performance is SEO twice over.

You don't need to chase a perfect Lighthouse score. A handful of fixes address most of the damage. Here they are, roughly in order of return.

Know the three metrics you're optimizing

  • LCP (Largest Contentful Paint) — when the biggest thing (usually your hero text or image) shows up. Target < 2.5s.
  • CLS (Cumulative Layout Shift) — how much stuff jumps around as the page loads. Target < 0.1.
  • INP (Interaction to Next Paint) — responsiveness to clicks/taps. Target < 200ms.

Most of what follows is really about LCP and CLS, because that's where content sites bleed.

Fix #1: Self-host your fonts

This is the most common single mistake and one of the easiest wins. The typical setup:

<link href="https://fonts.googleapis.com/css2?family=Inter..." rel="stylesheet" />

That one line does real damage:

  • It's render-blocking on a round-trip to a third-party origin — the browser opens a connection to Google, waits for CSS, then waits again for the font files, all before your text can paint. Straight LCP cost.
  • It leaks every visitor's IP to Google — a genuine GDPR concern in the EU, and a German court has already ruled against embedding Google Fonts this way.
  • It invites a flash of invisible/unstyled text if font-display isn't handled well.

Self-hosting removes the third-party request entirely. Modern frameworks make it a one-liner. In Next.js, next/font downloads the font at build time, self-hosts it, and injects a preloaded, zero-layout-shift @font-face:

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
});

No framework? Download the .woff2 files, host them yourself, and add <link rel="preload" as="font" ...> with font-display: swap. Confirm the third-party request is actually gone:

curl -s https://cv.lans.cloud/ | grep -c 'fonts.googleapis.com'
# 0  — no external font request

Fix #2: Give images and embeds explicit dimensions

The classic CLS culprit: an image loads, pushes everything below it down, and the user who was about to tap a link taps an ad instead. Always reserve the space:

<img src="hero.jpg" width="1200" height="630" alt="..." />

Also serve modern formats (avif/webp), lazy-load below-the-fold images (loading="lazy"), and eager-load the LCP image (fetchpriority="high", no lazy-load on that one — lazy-loading your hero hurts LCP).

Fix #3: Don't ship JavaScript to render static content

Every KB of JS is parse + execute time on the main thread, which hits INP and delays interactivity. The server-first architecture from Part 1 pays off again here: if your content is server-rendered, the client JS bundle only needs to cover the genuinely interactive bits, not re-render the whole page. Audit your bundle and ask whether that 200KB client component really needs to be a client component.

Fix #4: Respect the boring HTTP layer

  • Compression (gzip/brotli) on text responses — usually a one-line proxy setting.
  • Caching headers on static assets so repeat visits are instant.
  • HTTP/2 or HTTP/3 at the proxy — most reverse proxies (Traefik, nginx, Caddy) give you this for free.
  • A CDN if your audience is geographically spread.

None of these touch your application code; they live in your reverse proxy or host config, and they move real numbers.

The mindset

Performance work has diminishing returns, so spend your effort where the metrics actually hurt. Pull up PageSpeed Insights, read the "Opportunities" list, and fix top-down. For most sites the order above — fonts, image dimensions, JS weight, HTTP hygiene — clears the yellow-into-green jump with a day of work.

The last part closes the loop: how to verify everything in this series actually shipped, and how to keep it from silently regressing. Part 5: Verify & Iterate.


Next: Part 5: Verify & Iterate →