All posts

Performance

5 min read

15 June 2024

How we hit 100 on Lighthouse - every single time

It's not magic. Here's the checklist we run on every build before shipping.

Every site we ship at Titung scores 100 across all four Lighthouse categories — Performance, Accessibility, Best Practices, and SEO. Not most of the time. Every time.

Clients sometimes ask if we're gaming the score. We're not. The score is a proxy for doing the fundamentals right. When you get those right, the number follows.

Here's the checklist. These apply whether you're on React, WordPress, Drupal, or plain HTML.

1. Images are the biggest performance lever

Most sites fail Performance because of images. Fix images first.

  • Set explicit width and height on every <img>. This lets the browser reserve space before the image loads, eliminating Cumulative Layout Shift (CLS).
  • Use modern formats. WebP is supported everywhere. AVIF is even better where supported. Your build pipeline or CDN should handle conversion automatically.
  • Lazy-load below-the-fold images with loading="lazy". Never lazy-load hero images — the browser needs to fetch those immediately.
  • Preload your LCP image. The Largest Contentful Paint element (usually the hero image) needs a <link rel="preload"> in the <head>. Missing this is the single most common reason for an LCP score of 90 instead of 100.
<link rel="preload" as="image" href="/hero.webp" />

2. Load fonts without blocking the page

Google Fonts loaded as a <link rel="stylesheet"> in the <head> are a classic render-blocking culprit. Two fixes:

Option A — Self-host the font files. Download the WOFF2 files and serve them from your own domain. No third-party request, no DNS lookup, full control over caching.

Option B — Use font-display: swap in your CSS:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
}

swap tells the browser to show fallback text immediately and swap in the custom font when it arrives. Zero layout shift, zero render blocking.

If you must use a Google Fonts stylesheet link, at minimum add &display=swap to the URL.

3. Audit your JavaScript bundle

A large JS bundle delays Time to Interactive. Run a bundle analysis before every launch:

  • Remove libraries you don't use
  • Replace heavy dependencies with lightweight alternatives:
    • Moment.js (67kb) → date-fns or native Intl
    • Full Lodash → individual lodash-es imports or native equivalents
    • Full icon libraries → import only the icons you use
  • Code-split aggressively. Load JavaScript for a feature when the user needs it, not upfront.

If a single third-party package is larger than your entire application code, that's a problem.

4. Eliminate Cumulative Layout Shift

CLS is the score that punishes you for things moving around after load. Common causes:

  • Images without dimensions: always set width and height attributes (see above)
  • Fonts swapping: handled by font-display: swap
  • Dynamic content injected above the fold: if a cookie banner, notification bar, or chat widget appears above page content, reserve its space with CSS before it loads. Use min-height on the container.
  • Embeds (video players, maps, social widgets) that calculate their own height after render — wrap them in a container with a fixed aspect ratio

Lighthouse measures CLS over the page lifetime, not just initial load. Scroll-triggered animations that shift content count too.

5. Defer third-party scripts

Analytics, chat widgets, and A/B testing tools are the biggest LCP killers on otherwise well-optimised sites. The rule is simple: nothing from a third-party domain should block the main thread during page load.

Load third-party scripts with defer or async:

<!-- Loads after HTML is parsed, executes in order -->
<script src="https://analytics.example.com/script.js" defer></script>

<!-- Loads in parallel, executes as soon as ready — use for truly independent scripts -->
<script src="https://widget.example.com/embed.js" async></script>

Never load analytics in a <script> tag without defer or async unless the script genuinely must run before any rendering (it almost never does).

6. Set proper cache headers

Fast first loads are good. Instant repeat loads are better. Make sure static assets — fonts, images, JS, CSS — have long cache lifetimes:

Cache-Control: public, max-age=31536000, immutable

Use fingerprinted filenames (e.g. main.a3f2c1.js) so you can set one-year cache expiry safely. When you deploy new code, the filename changes and the browser fetches fresh.

For HTML and API responses, use a shorter cache with stale-while-revalidate so users get instant loads while the cache refreshes in the background.

7. Check accessibility — it affects your score

The Accessibility category catches things that are often treated as "nice to have" but are scored as bugs:

  • Every <img> needs an alt attribute (empty string is valid for decorative images)
  • Form inputs need <label> elements
  • Colour contrast must meet WCAG AA minimums
  • Interactive elements must be keyboard-navigable
  • Page must have a <title> and an <h1>

Fix these and you pick up points in both Accessibility and Best Practices simultaneously.

8. Run Lighthouse in CI, not just locally

Local Lighthouse scores are noisy. Your machine's CPU throttling simulation isn't consistent, and scores can vary by 5–10 points between runs. Use Lighthouse CI on every pull request:

# .github/workflows/lighthouse.yml
- name: Run Lighthouse CI
  uses: treosh/lighthouse-ci-action@v10
  with:
    urls: |
      https://preview-url.example.com/
    budgetPath: ./lighthouse-budget.json
    uploadArtifacts: true

Set a budget file that fails the build if any score drops below 95. This prevents regressions from sneaking in.

The mindset shift

The teams that consistently hit 100 aren't doing anything exotic. They've internalised the principle that performance is a feature, not a polish pass at the end. Every time you reach for a heavy library, you should feel the cost. Every time you load a third-party script, you should ask if it's really worth it.

Good Lighthouse scores are a symptom of that mindset, not the goal itself.

This is exactly what our Speed Optimization service covers - a full audit against this checklist, run on your existing build, with a before/after report.


We run this checklist on every Titung project before launch. If you're inheriting a slow site or starting fresh and want to do it right, get in touch.

Work with us

Want us to do this for your product?

We build sites that hit 100 on Lighthouse by default, not by accident. Tell us about your project.