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-fnsor nativeIntl - Full Lodash → individual
lodash-esimports or native equivalents - Full icon libraries → import only the icons you use
- Moment.js (67kb) →
- 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
widthandheightattributes (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-heighton 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 analtattribute (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.
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.