· Web Development

Why a static site starts at 97 on Lighthouse, and what the last point costs

Our site scores 100 on desktop and 99 on mobile. Most of that came from the stack we chose, and the last point is one we decided not to buy.

Run this site through PageSpeed Insights and you get 100 on desktop and 99 on mobile, with Accessibility, Best Practices and SEO at 100 on both.

PageSpeed Insights desktop report for orbitalwebsolutions.co.uk: Performance 100, Accessibility 100, Best Practices 100, SEO 100

We would like to take credit for a clever optimisation pass. The truth is more useful: almost all of it was decided before we wrote a line of CSS, when we chose how the site would be built.

Where the first 97 came from

Mobile started at 97 without any tuning. Nothing on the list below is a performance trick. Each one is a decision about what to leave out.

  • The pages are plain HTML. The site is built once, ahead of time, and served as finished files. There is no application server assembling a page per request, no database query and no plugin stack running before the first byte.
  • One stylesheet. About 46 KB on disk, under 10 KiB over the wire once compressed. No framework, no utility-class build, no CSS shipped for components that are not on the page.
  • Fonts we host ourselves. Three preloaded WOFF2 files, served from our own domain. No third-party font service, which means no extra DNS lookup, no extra connection and no layout jump while they arrive.
  • Almost no JavaScript. A small Stimulus layer with five controllers (accordion, budget slider, contact form, headline, notifications), each doing one job. Nothing renders on the client that the server could have rendered already.
  • Nothing third-party on the critical path. Lighthouse’s dependency tree for the home page lists our HTML, our CSS, our JS and one Cloudflare script (more on that below). That is it.
  • Real HTML structure. Landmarks, headings in order, a skip link, labelled form fields. That is why Accessibility and SEO sit at 100 without a separate effort.

None of that is accidental. It is what you get when a site is treated as a document first and an application second, because that is what a services site is.

The housekeeping pass

On top of that baseline we made one set of changes. They are small, and worth listing because they are the kind of thing most sites skip.

Minify the vendored JavaScript. The copy of Stimulus we ship was unminified at 88,680 bytes. Minified it is 45,841 bytes, a saving of about 48% for no change in behaviour.

Cache-bust every asset with a version query. The stylesheet, the script and each module now carry ?v= followed by the build timestamp. A new deploy gets new URLs, so the old ones can be cached for a long time without ever serving stale files. A Cloudflare cache rule now serves the CSS, JavaScript and fonts with a one-year lifetime. Fonts never change in place, so they are safe for the same reason. Lighthouse’s cache audit now reports nothing of ours to save.

Load modules through an import map, and preload them. Previously application.js imported each controller by relative path, so the browser had to download the entry file before it could start fetching the six modules it names. An import map lets the code say import { Application } from "stimulus" while the page decides the real URL, which is what allows every module URL to carry the ?v= (a relative import cannot). modulepreload then tells the browser to fetch all six modules in parallel from the start, without waiting for the entry file.

<script type="importmap">
{
  "imports": {
    "stimulus": "/assets/js/vendor/stimulus.js?v=1760443000",
    "controllers/accordion": "/assets/js/controllers/accordion_controller.js?v=1760443000"
  }
}
</script>
<link rel="modulepreload" href="/assets/js/vendor/stimulus.js?v=1760443000">

There is no bundler and no build step for the JavaScript. The browser’s own module loader does the work.

The last point, and why we are leaving it

Mobile reports 99, not 100. Lighthouse tells us exactly why: render-blocking requests, estimated saving 150 ms. There are two of them.

Request Size over the wire Duration
main.css 9.8 KiB 150 ms
Cloudflare email-decode.min.js 1.2 KiB 150 ms

The longest critical path is 637 ms. FCP is 1.1 s and LCP is 2.1 s on throttled mobile, both green. The remaining point is real, and we could have it. We looked at what each fix would cost.

Inlining the CSS. Putting the stylesheet in the <head> removes the request. It also means every page carries its own copy of the CSS, uncached, and the browser can never reuse it from the page before. For a visitor who reads three pages that is roughly 30 KiB downloaded instead of one cached 10 KiB file, and it gives up the long cache lifetimes we just set up.

Dropping the email obfuscation. The second blocking request is the script that decodes our contact address in the browser. It exists so the address is not sitting in the HTML for scrapers to harvest. Removing it would make Lighthouse happier and put the address in plain sight of every simple scraper. We would rather keep the protection and take the 150 ms.

Both fixes are available to us. Neither is worth what it costs. A score is a proxy for a good experience. Paying real quality to improve the proxy is a bad trade.

What the number does not tell you

  • It is a lab score. One simulated phone on a simulated connection. PageSpeed shows “No Data” for our real-user field data because the site does not have enough traffic yet. Treat the score as a health check, not a measurement of your visitors.
  • It is fragile. One tag manager, chat widget or ad pixel can undo all of this. Staying fast is mostly staying disciplined about what gets added.
  • It is not the whole job. A fast page that does not say clearly what you do, or that is hard to contact, has missed the point.

Why we build this way

We are not arguing that every site should be static. Web applications need servers, databases and logic, and we build those too. But for a marketing or services site, where the content changes weekly rather than by the second, the dynamic machinery is mostly cost. Take it away and good performance stops being a project and becomes the starting point.

That is the thing worth knowing if you are comparing quotes. A site that has to be made fast is a site that is slow by default, and will drift back there as plugins and scripts are added. A site that is fast by construction stays fast with much less effort.

If you want to see how that applies to your own site, get in touch and we will run it through the same checks and tell you what we find.

← All posts

Or get in touch directly

How would you like a reply?

You’ll get a reply within one working day.