A perfect Lighthouse score has a slightly silly reputation: a vanity badge that means little in the real world. There is some truth to that, but it misses the point. Chasing 100/100 forces you to read what Lighthouse actually measures, and most of those audits are real-user concerns wearing a score: how fast the page paints, whether the layout jumps around, whether a screen reader can use it, whether a crawler can index it.

Lighthouse reports four category scores out of 100: Performance, Accessibility, Best Practices and SEO. Three of them are largely deterministic checklists you can lock at 100 and keep there. Performance is the awkward one: it is a weighted blend of simulated metrics that drifts every time you touch the page, and it is where almost all the work lives.

This is a generalised account of getting all four to 100 on a real site (this one: a static marketing site on Cloudflare Pages), written for someone researching how to do it on theirs. The stack barely matters. What matters is the order you work in and how tight your feedback loop is, so that is what this is built around.

// WHO THIS IS FOR

Best fit: content and marketing sites, docs, blogs, and landing pages, where 100/100/100/100 is genuinely achievable.

Harder: heavy single-page apps. The Accessibility, Best Practices and SEO methods still apply directly; mobile Performance 100 with a large JavaScript bundle is a much bigger fight than anything described here.

What a perfect score is actually worth

Before the work, it is worth being clear about why a lab score earns the effort, because "because it is satisfying" does not survive contact with a roadmap. Speed is a commercial lever in three separate places, and a perfect Lighthouse score is the cheap, legible proxy for all three.

It moves conversion rate. Google's research with Deloitte (Milliseconds Make Millions, 2020) found that a 0.1 second improvement in mobile load time lifted retail conversion rates by 8.4% and average order value by 9.2%, with comparable gains in travel and lead generation. The mechanism is blunt: Google's earlier work put the probability of a bounce up 32% as load time goes from one to three seconds, and up 90% by five. Every page in the funnel that paints faster keeps more of the people who were already going to convert.

It lowers the cost of paid media. Landing page experience, which is largely speed and stability, feeds Google Ads Quality Score and therefore Ad Rank. A faster, steadier landing page can win a lower cost per click for the same position, and it stops you paying for clicks that bounce before the page is usable. On a paid acquisition account (the S02 Paid Media work), the landing page is where media budget is either converted or wasted, so its Core Web Vitals are a media-efficiency line item, not a developer nicety.

It is an organic ranking input. Core Web Vitals (Largest Contentful Paint, Cumulative Layout Shift, and Interaction to Next Paint) are part of Google's page experience ranking signals. The subtlety, and the bridge to the next section, is that the ranking signal uses field data from real Chrome users, not the lab Lighthouse score. The 100 is not the thing Google ranks; it is the controllable proxy you optimise so that the field metrics, the ones that actually rank, follow it down.

There is a fourth angle that sits closest to home for a measurement practice. Every third-party tag (the tag manager, analytics, pixels, the consent banner itself) is a speed cost, so "get to 100" forces an honest conversation about which tags earn their performance tax and how they load. That is a tracking and attribution decision (S03), not only an engineering one: the goal is to keep the measurement while taking its weight off the paint the conversion depends on. The deferred tag manager in the Performance section is exactly that trade, and it is the same tension L/001 works through from the consent side.

Why 100/100 is a moving target

The first thing to internalise is that Lighthouse is a lab tool. It loads the page in a controlled environment, applies simulated throttling (a slower CPU and network than your machine), and models the metrics. That is why the same page can score 100 on your laptop and 90 in PageSpeed Insights: PSI runs the mobile profile with heavier throttling. Always judge yourself against the throttled mobile run, because that is the one that is hard to fool.

Three of the four categories behave like pass/fail checklists. Accessibility, Best Practices and SEO are mostly binary audits: a given check passes or it does not, and once you have fixed it, it stays fixed unless you regress the markup. You can take those three to 100 and largely forget them.

Performance is different. It is a weighted score built from First Contentful Paint, Largest Contentful Paint, Total Blocking Time, Cumulative Layout Shift and Speed Index, each modelled by a simulation. Change one font preload and the LCP estimate moves; defer one script and Total Blocking Time drops but a layout shift might appear instead. It drifts constantly, which is exactly why the loop in the next section matters more than any single fix.

One more trap before the work: do not score a single template and declare victory. Test a representative page from each kind you ship. On this site that meant the homepage, a content page, and a long article, because the article template carried render-blocking CSS the homepage had already shed, and it was the only place a particular layout shift showed up.

The loop: measure, fix, re-measure

The single biggest accelerator is not a clever optimisation; it is shrinking the gap between making a change and seeing its effect. The slow way is to edit, deploy, run PageSpeed Insights, squint at the headline number, guess, and repeat. Each turn of that loop is minutes long and tells you almost nothing about why the number moved.

The fast way runs Lighthouse and throttled performance traces locally, in Chrome DevTools, against the page in front of you. A turn of that loop is seconds, and the trace tells you which audit is costing points and, crucially, which specific DOM node is responsible. I drove this with the Chrome DevTools MCP so the agent could run a throttled trace, read the audit JSON directly, and attribute a metric to an element without me clicking through panels: "CLS 0.208, attributed to div.article-grid, cause: media element lacking an explicit size" is a one-line fix once you can see it.

// THE FEEDBACK LOOP 01 MEASURE throttled trace 02 ATTRIBUTE audit + DOM node 03 FIX smallest change 04 RE-MEASURE same page, same throttle loop until the metric holds and nothing else has regressed // THE FEEDBACK LOOP 01 MEASURE throttled trace 02 ATTRIBUTE audit + DOM node 03 FIX smallest change 04 RE-MEASURE same page, same throttle loop until it holds
The loop runs locally and throttled, so each turn is seconds. Node-level attribution is what turns a vague score into a single concrete fix.

The discipline is the same one a good data team uses to trust a number before acting on it; the eval harness in L/010 is the same idea applied to analytics: measure, prove, re-measure. Each turn here is: trace a representative page under a fixed throttle (4x CPU, Slow 4G, mobile); read the top opportunity and the element behind it; make the smallest change that addresses that element; re-trace the same page under the same throttle; confirm the number moved and that nothing else regressed.

// READ THE SAVINGS, NOT THE SCORE

Every opportunity audit reports a modelled saving. A render-blocking stylesheet that Lighthouse says will save 0 ms is not worth moving off the critical path, however much the word "render-blocking" itches. Treat the per-audit savings as the source of truth and the headline number as a summary; chasing the summary directly is how you trade a real metric for a vanity one.

Confirm the final result with one canonical PageSpeed Insights run per page. DevTools and PSI both use the same simulation engine, but PSI is the score people will quote back to you, so the claim "100/100" should rest on it, not on a single local pass.

// WHAT MOVED THE SCORE ON THIS SITE

If you want the short version before the reasoning, the loop produced six changes, each unpacked in the sections below:

  • Inlined the above-the-fold critical CSS and async-loaded the rest.
  • Preloaded the above-the-fold font weights at high priority.
  • Deferred the tag manager and analytics until after first paint.
  • Reserved explicit space for every lazy image, to stop layout shift.
  • Fixed one low-contrast meta-label token for Accessibility.
  • Re-ran the throttled Lighthouse trace after every change.

Performance: the critical rendering path

On a content site, almost all of the Performance score comes down to the critical rendering path: what the browser must fetch and execute before it can paint, and whether the layout stays still while it does. Three levers do most of the work.

Render-blocking CSS. A normal <link rel="stylesheet"> blocks the first paint until it downloads. The fix is to inline the small slice of CSS needed for above-the-fold content and load the rest asynchronously. On this site every inner page still shipped its full stylesheet render-blocking while the homepage had already moved to inline-critical-plus-async; extending that pattern cleared the roughly 940 ms of modelled render-blocking savings PageSpeed flagged on an article, where 666 ms of a 705 ms LCP was pure render delay waiting on those requests.

// html · inline the critical slice, async-load the rest
<style>/* critical: tokens, nav, hero, above-the-fold layout */</style>

<link rel="preload" href="/assets/css/site.a84783d8.css" as="style"
      onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/css/site.a84783d8.css"></noscript>

But measure the trade rather than asyncing everything by reflex. A second, smaller stylesheet on the article template was about 5 KB compressed and modelled roughly 0 ms of render-blocking savings. Moving it off the critical path bought nothing and, as the regressions section explains, cost a layout shift. The rule that fell out: async the large stylesheet, keep tiny ones blocking so first paint is fully styled.

Fonts. A web font that arrives late hurts twice: if it styles the largest text it delays LCP, and when it swaps in it shifts the layout. Self-host the woff2 files, and preload the specific above-the-fold weights with fetchpriority="high" so they land before first paint instead of being discovered late inside a stylesheet.

// html · preload above-the-fold weights at high priority
<link rel="preload" href="/assets/fonts/Heading-Bold.woff2"
      as="font" type="font/woff2" crossorigin fetchpriority="high">

Third-party JavaScript. Tag managers and analytics are usually the heaviest thing on a marketing page, and almost none of it is needed for first paint. Defer the script fetch until the main thread is idle. On this site the tag manager and analytics bundle (around 270 KB, of which PageSpeed flagged roughly 126 KB unused at first paint) loads via requestIdleCallback after LCP, with a timeout fallback, so the data layer still initialises immediately but the download never competes with the paint. The caveat worth stating for server-side setups: deferring the client tag can cost you the browser-side copy of an early event on a fast bounce, but it will not break deduplication, because Meta CAPI and its equivalents match the browser and server events on a stable event_id within a 48-hour window that a few-seconds deferral sits well inside; fire conversions on user action and treat the server event as the source of truth.

Images. Lazy-load anything below the fold, but always give an image an explicit width and height (or an aspect-ratio box) so the browser reserves space before it loads. A lazy image with no reserved box is the most common Cumulative Layout Shift on an otherwise clean page, and it is exactly the one that bit here once first paint got fast.

Accessibility: the cheapest 100 of the four

Accessibility is the category most sites leave at 96 or 98 for no good reason, because the misses are small and mechanical. Lighthouse runs a subset of automated checks (it cannot test everything, so a 100 here is a floor, not a certificate), and that subset is easy to clear. The common point-losers, in rough order of how often they appear:

AuditTypical causeFix
Colour contrastmuted label text below 4.5:1darken the token to at least WCAG AA 4.5:1
Image alt textmissing alt, or alt on decorative imagesdescribe meaningful images; alt="" on decorative ones
Heading orderskipped levels (h2 to h4)one h1, then sequential h2 / h3, no jumps
Link / button nameicon-only control with no labelvisible text or an aria-label
Document langmissing html lang<html lang="en-GB">

Contrast is the one that quietly caps the score. On this site a pair of small meta labels sat at 2.29:1 against the dark background, a trade-off that had been consciously accepted for the look, and it alone held Accessibility at 97 on the article template. Moving them to a token that measured 5.62:1 (still muted, still clearly secondary to the body text) landed the 100 without changing the hierarchy. The lesson is that "accepted" contrast debt is usually a single token away from being paid off.

Beyond the automated audits, two things cost nothing and matter more to real users than the score: a working "skip to main content" link, and proper landmark structure (nav, main, footer) so a keyboard or screen-reader user can move around. Build those in once and they never regress.

Best Practices: stop losing easy points

Best Practices is a hygiene checklist, and on a cleanly built site most of it is free. It is worth knowing the handful of audits that catch people out, because they are usually accidents rather than design decisions.

  • HTTPS with no mixed content. One asset requested over http:// on an https:// page fails the audit. Make every internal reference root-relative or absolute-https.
  • No console errors. Lighthouse fails the page if anything logs an error, and the usual culprit is a 404 on a missing asset or a third-party script throwing. Open the console on the throttled run and clear it.
  • A viewport that allows zoom. A viewport meta that sets maximum-scale=1 or user-scalable=no is both an accessibility and a best-practice fail. Use width=device-width, initial-scale=1 and nothing else.
  • No deprecated APIs and a valid doctype. A standards-mode <!doctype html> and avoiding deprecated browser APIs covers the rest.
  • Images at their natural aspect ratio. Serving an image at a different ratio to its real dimensions trips a Best-Practices audit as well as shifting layout; the explicit width and height from the Performance section fixes both.

Security headers (a content security policy, sensible cache and transport headers) do not all show up directly in the Lighthouse number, but they are the same class of hygiene and worth setting in the same pass while you are already in the headers file.

SEO: the technical baseline

Lighthouse SEO is the most misunderstood of the four. It does not measure whether you will rank; it measures whether a crawler can reach, read, and index the page at all. It is a technical baseline, and it is cheap to lock at 100, which makes it the floor everything else sits on.

  • Title and meta description present and unique. One descriptive <title> and one <meta name="description"> per page.
  • Indexable. No accidental <meta name="robots" content="noindex"> left over from staging, and a robots.txt that does not block the page.
  • A valid canonical. An absolute, self-referential <link rel="canonical"> so duplicate URLs consolidate to one.
  • Descriptive link text. "Read the method" beats "click here"; Lighthouse flags links whose only text is generic.
  • Legible on mobile. Font sizes large enough to read and tap targets far enough apart that the audit passes.

Structured data is not strictly required for the SEO score, but Lighthouse will flag invalid structured data if you ship it, so validate any JSON-LD you add. The wider point is that this category overlaps almost entirely with publishing discipline: a correct canonical, a clean title, a valid sitemap entry. Get the publishing checklist right and SEO scores 100 as a side effect.

One clarification that trips people up: the speed ranking signal does not live in this category. The Lighthouse SEO score is the crawlability baseline; the part of search that rewards speed is Core Web Vitals, which sit under Performance and feed page experience separately (the commercial case above). Both matter, for different reasons, which is why this guide treats them as different jobs.

Three of the four categories are checklists you clear once. Performance is the one that drifts, so the score that proves you have it under control is the throttled mobile one, re-run after every change.

The regressions the loop caught

Here is the part that justifies the whole loop. Making first paint fast does not just raise the score; it changes the page's behaviour, and it can expose problems the slow build was accidentally hiding. The critical-CSS work above introduced two separate layout-shift regressions, both caught and fixed the same day, only because every change was re-measured.

Regression one: the font swap (mobile). After inlining critical CSS, a PageSpeed mobile run showed Accessibility fixed and render-blocking cleared, but Cumulative Layout Shift had jumped from 0 to 0.20. The trace attributed it to the masthead heading and the deck paragraph, with the cause "Web font loaded". The mechanism: the old render-blocking build delayed first paint until the preloaded fonts had arrived, so the swap was invisible; making first paint fast meant the page now painted in the fallback font and shifted when the real weights swapped in. The fix was to promote those two specific font preloads to fetchpriority="high" so they arrived before first paint. Mobile CLS went back to 0.

Regression two: the lazy image (desktop). The same article then scored Performance 89 on desktop, dragged entirely by CLS 0.209 attributed to the body grid, with the sub-cause "media element lacking an explicit size". The culprit was a below-the-fold diagram loaded with loading="lazy" and no reserved box. When the body stylesheet had been render-blocking, the whole body was styled before first paint and the lazy image never reflowed inside the measurement window. Asyncing that stylesheet deferred the body styles, so the image now arrived late and pushed the layout down. The fix was the small-stylesheet rule from the Performance section: keep the body stylesheet render-blocking (it modelled near-zero savings anyway) and only async the large one. Desktop CLS went back to 0 and Performance to 100.

A faster first paint does not create these shifts; it stops hiding them. The loop is what turns "the score dropped" into "this element, this cause, this line".

Neither of these was predictable from the change itself. Both were obvious within one re-measure once the trace named the element. That is the entire argument for the tight loop: optimisations interact, and the only cheap way to keep four numbers at their target at once is to re-check all of them after every change.

What I deliberately left alone

Knowing when to stop is part of the method. A perfect score is not worth a real regression, and some remaining "opportunities" are measurement artefacts or someone else's asset. After the regressions were closed, the result settled here:

// DESKTOP PERF100/ 100
// MOBILE PERF96/ 100
// A11Y / BP / SEO100EACH
// CLS0BOTH

Desktop is 100/100/100/100. Mobile sits at 96 on Performance, and I stopped there on purpose. The single remaining drag was a simulated Largest Contentful Paint on a text element, with a 326 ms modelled font-swap render delay. The only lever left was a metrics-matched fallback font for that element, and every prior attempt at that exact trick had regressed CLS and been rolled back. Lighthouse modelled the LCP saving as 0 ms: a near-zero gain for a real risk to a metric already sitting at 0. That is not a trade worth making for four points on one profile.

Two other flagged items were left alone for the same kind of reason. A small forced-reflow cost is the intrinsic price of the first layout; wrapping it differently just relabels it without making it faster. And a third-party analytics beacon's cache lifetime is set by the vendor, not by me. Recognising that an opportunity is an artefact or out of your control is as much a skill as fixing the ones that are not.

The honest summary is that 100/100/100/100 is mostly a discipline problem, not a cleverness one. Three of the four categories are checklists you clear once and keep. Performance is the one that drifts, and the thing that holds it at 100 is not a magic optimisation; it is a feedback loop tight enough that a regression is caught the same hour it is introduced, with a trace that names the element so the fix is a single line. And because the score is a standing proxy for conversion rate, paid-media efficiency, and organic visibility, keeping it there is not gold-plating; it is protecting three revenue lines at once.