EmpireUI
Get Pro
← Blog9 min read#lighthouse#performance#audit

Lighthouse Performance Audit: Every Score, Every Fix Explained

Run Lighthouse, get a score, now what? This guide walks every metric, every red flag, and the exact fixes that actually move the needle.

Lighthouse performance audit scores dashboard in Chrome DevTools

What Lighthouse Is Actually Measuring

A lot of developers treat Lighthouse like a report card — you run it, you screenshot the score, you move on. That's backwards. The score is a weighted average of five underlying metric buckets, and if you don't know which bucket is tanking you, you're guessing at fixes. Lighthouse 12 (shipping alongside Chrome 124 in early 2024) reshuffled those weights pretty significantly, so older advice you've read might be sending you in the wrong direction.

The five lab metrics are: First Contentful Paint (FCP), Largest Contentful Paint (LCP), Total Blocking Time (TBT), Cumulative Layout Shift (CLS), and Speed Index. In the current scoring model, TBT alone carries 30% of the weight. That's huge. A slow main thread will destroy your score even if your assets are perfectly optimized — something worth keeping in mind before you spend three hours compressing PNGs.

Quick aside: Lighthouse runs in a simulated mobile environment by default, applying a 4x CPU slowdown and throttling network to approximately 10 Mbps download. Your local machine is probably 8–12x faster than that baseline. So a score of 72 in the panel doesn't mean the site is broken — it means the site is slow on a mid-range phone on a decent 4G connection. Know what you're measuring before you panic.

Worth noting: Lighthouse lab scores and your real-user Core Web Vitals (from Chrome UX Report) will diverge. Both matter. Lab scores help you iterate fast in dev. Field data (available in PageSpeed Insights) tells you what real users actually experience. Use both.

Reading the Report Without Getting Lost

Open Chrome DevTools, hit the Lighthouse tab, run it. You'll get a colored circle — green above 90, orange 50–89, red below 50. Below the score are opportunities, diagnostics, and passed audits. Most people click the first red opportunity and start there. Don't. Scan the diagnostics section first, because it surfaces systemic issues that often explain multiple failing opportunities at once.

The opportunities section shows estimated savings in seconds. Treat these as upper bounds. Lighthouse calculates them assuming ideal conditions — every byte you eliminate saves the full transfer time. Reality is messier. That said, anything showing >500ms potential savings is worth taking seriously.

Honestly, the most useful thing in the report is the filmstrip at the top. Those screenshot thumbnails at 0.3s, 0.6s, 0.9s intervals show you what users actually see during load. If your page is a white void until 2.1 seconds, that's a skeleton or SSR problem, not a JavaScript bundle problem. The filmstrip gives you a visual diff you can't get from numbers alone.

One more thing — the treemap view (accessible from the 'View Treemap' link in the report) renders your JavaScript bundles as a visual grid weighted by size. It's the fastest way to spot a 400kb library being imported for a 3-line utility function. Pop that open before you touch any webpack or Vite config.

Fixing LCP: The Metric That Matters Most for SEO

LCP measures when the largest visible element renders. Usually it's a hero image or an H1. Google has used LCP as a ranking signal since the Page Experience update in 2021, and they've made it clear they're not backing off. If your LCP is above 2.5 seconds, you're leaving ranking points on the table — full stop.

The most common LCP killers, in rough order of frequency: unoptimized hero images (still JPEG when they should be WebP or AVIF), render-blocking CSS in <head>, late-loading fonts that delay text rendering, and server response times above 600ms. Start with the image. Add fetchpriority="high" to your LCP image element — it's a single attribute and it tells the browser to prioritize that request above everything else in the queue.

<!-- Before: browser treats this like any other image -->
<img src="/hero.jpg" alt="Hero" />

<!-- After: browser fetches this before anything else in the queue -->
<img
  src="/hero.webp"
  fetchpriority="high"
  decoding="async"
  width="1200"
  height="630"
  alt="Hero"
/>

If you're on Next.js, next/image with priority prop does this automatically — but only if you've set it. I've seen dozens of sites using next/image everywhere except the hero, then wondering why LCP is 3.8 seconds. The nextjs-image-optimization patterns article covers this in depth if you're using the App Router.

Font loading is the sneaky LCP killer. If your LCP element is a heading that uses a custom web font, and that font isn't preloaded, the browser has to: parse HTML, discover the <link> or CSS @font-face, request the font, download it, then render the text. That chain adds 800–1200ms on mobile networks. Preload critical fonts explicitly: <link rel="preload" as="font" href="/fonts/Inter-Regular.woff2" crossorigin>.

Crushing TBT: The Hidden Score Killer

Total Blocking Time is the sum of all time periods where the main thread was blocked for more than 50ms during the FCP-to-TTI window. It's the lab proxy for First Input Delay (FID) and Interaction to Next Paint (INP). And because it carries 30% of the Lighthouse score weight, shaving 200ms off TBT is often worth more to your score than trimming 500ms off LCP.

Long tasks on the main thread are the culprit. A 180ms task blocks the main thread for 130ms (the portion over 50ms). Ten of those and you've got 1.3 seconds of TBT. The Chrome Performance tab shows long tasks as red bars — run a profile during page load and look for anything red in the main thread lane. The Lighthouse report also lists specific scripts causing long tasks under the 'Avoid long main-thread tasks' diagnostic.

// Instead of doing everything synchronously on load:
window.addEventListener('load', () => {
  initAnalytics();    // 80ms
  loadChatWidget();   // 120ms
  setupHeatmaps();    // 95ms
  // Total: 295ms blocking task
});

// Defer non-critical work into idle slices:
window.addEventListener('load', () => {
  // Only block for what users immediately need
  initAnalytics();
  
  // Push the rest to idle callbacks
  requestIdleCallback(() => loadChatWidget());
  requestIdleCallback(() => setupHeatmaps());
});

Third-party scripts are almost always the worst offenders. Analytics, A/B testing tools, chat widgets, heatmaps — they ship their own large JS bundles and they run on your main thread. Look — there's no polite way to say this: a full-featured chat widget can add 400–600ms of TBT by itself. Load them with async or defer, or better yet, load them after a user interaction using an Intersection Observer or a click/scroll event listener.

In practice, the fastest TBT wins usually come from splitting your own bundle. Vite and webpack both do automatic code splitting, but they won't split unless you give them explicit boundaries. Use dynamic imports (import()) for routes, modals, and anything that isn't visible on initial load. A single-route React app with a 280kb bundle can become a 60kb initial load if you split aggressively.

Fixing CLS: Stop Your Layout From Jumping

Cumulative Layout Shift scores the visual stability of your page. Every time something moves unexpectedly — an image that loads and pushes text down, an ad slot that appears and shifts your CTA button, a font swap that reflows a paragraph — CLS accumulates. Google wants you below 0.1. Above 0.25 is red. The frustrating part is CLS bugs are invisible during development because you have things cached and loaded instantly.

The number one CLS fix is dead simple: set explicit width and height attributes on every <img> and <video>. Browsers use these to reserve space before the asset loads. Without them, the browser allocates zero height, content fills in below, the image loads at 400px tall, and everything shifts down 400px. That one oversight causes the majority of CLS issues on most sites.

/* Reserve space for images with aspect-ratio */
.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto; /* browser honors the reserved space */
}

/* For dynamically injected content, pre-allocate the slot */
.ad-slot {
  min-height: 250px; /* reserve the most common banner height */
  contain: layout; /* prevent layout from escaping the box */
}

Font swap causes CLS too. When font-display: swap kicks in and your fallback font is 12px wider per character than your web font, every line reflows. The fix is font-display: optional for non-critical fonts (which skips swap entirely if the font isn't loaded in time) or using size-adjust, ascent-override, and descent-override CSS descriptors to make your fallback font match the web font's metrics exactly. The font-loading-web article on this blog has a worked example with exact values.

Dynamic content injection — banners, cookie notices, chat bubbles — is the last major CLS category. If you inject content above the fold after load, you shift everything below it. Always inject above-the-fold dynamic content into pre-reserved slots. For cookie banners specifically, use position: fixed or position: sticky so they overlay rather than displace content.

The Accessibility and SEO Tabs Aren't Optional

Most people close Lighthouse after checking the Performance tab. That's leaving half the value on the table. The Accessibility score directly feeds into SEO — Google uses accessibility signals as part of quality assessment — and the SEO tab catches technical issues that can tank crawlability regardless of how fast your page loads.

The Accessibility tab runs about 40 automated checks from the Axe rules library. Common failures: images without alt text, form inputs without labels, insufficient color contrast (the threshold is 4.5:1 for normal text, 3:1 for large text), interactive elements that can't be focused with a keyboard, and missing ARIA landmarks. These aren't just checkboxes — they represent real users who can't use your product. The react-accessibility-guide has patterns for the common failures that show up across component libraries.

The SEO tab checks for <meta name="description">, crawlable links, legible font sizes (minimum 12px), tap targets large enough for touch (48x48px minimum per Google's 2024 guidelines), and robots.txt / <meta name="robots"> validity. Small stuff that's trivially fixable but surprisingly common in production. If you're building with Empire UI's templates, most of this is handled by default — but if you're rolling custom layouts, you need to check these manually.

Worth noting: Lighthouse's SEO score doesn't replace Search Console. It checks for technical accessibility to crawlers, not for content quality, structured data richness, or actual indexing status. A 100 SEO score in Lighthouse and a 100 in Performance won't save a page with thin content and no inbound links. Use Lighthouse for the technical layer, use Search Console for the indexing and click-through layer.

One more thing — the Best Practices tab. It catches HTTPS issues, browser console errors, deprecated APIs, and unsafe cross-origin practices. A common hit: loading third-party resources over HTTP on an HTTPS page generates a mixed-content warning that drops your Best Practices score to 78 instantly. Check it.

Running Lighthouse in CI So You Stop Regressing

The hardest part of performance work isn't the initial fix — it's keeping it fixed. Someone ships a new marketing component with a 340kb uncompressed video background, and suddenly your LCP goes from 1.8s to 4.2s and nobody notices for three weeks. Lighthouse CI prevents that. It runs audits on every PR and fails the build if scores drop below your thresholds.

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          urls: 'http://localhost:3000'
          budgetPath: './lighthouse-budget.json'
          uploadArtifacts: true
          temporaryPublicStorage: true
// lighthouse-budget.json
[
  {
    "path": "/*",
    "timings": [
      { "metric": "interactive", "budget": 3500 },
      { "metric": "first-contentful-paint", "budget": 1500 },
      { "metric": "largest-contentful-paint", "budget": 2500 }
    ],
    "resourceSizes": [
      { "resourceType": "script", "budget": 300 },
      { "resourceType": "image", "budget": 500 },
      { "resourceType": "total", "budget": 900 }
    ],
    "resourceCounts": [
      { "resourceType": "third-party", "budget": 10 }
    ]
  }
]

That budget file is opinionated but realistic. 300kb of JavaScript and 900kb total page weight will get you green scores on mobile. If your team regularly ships pages above those numbers, the budget will catch it in PR review rather than post-deploy. The script budget alone will force conversations about third-party bloat that you'd otherwise never have.

If you're using Vercel, the Speed Insights integration gives you field data automatically — real user LCP, FCP, and CLS pulled from browser instrumentation. That's the data that actually correlates with ranking signals. Pair it with Lighthouse CI for lab data and you've got both layers covered without manual auditing. For component-level performance patterns, check out the react-performance-guide which goes deeper on memoization, virtualization, and bundle splitting strategies that complement what Lighthouse surfaces.

FAQ

Why does my Lighthouse score differ between runs on the same page?

Lighthouse uses CPU and network throttling that's sensitive to your machine's load. Background processes, browser extensions, and varying network conditions all affect results. Run Lighthouse in incognito with extensions disabled, or use the CLI for more consistent results.

Does a higher Lighthouse score directly improve Google rankings?

Core Web Vitals (LCP, CLS, INP) are a confirmed ranking signal, but Lighthouse's lab score is a proxy — not the field data Google uses. A good lab score generally means good field CWV, but focus on field data in Search Console for actual ranking impact.

What's the fastest single fix that usually moves the Lighthouse score the most?

Deferring or eliminating third-party scripts. A single chat widget or A/B testing tool can add 300–600ms of TBT, which swings the score by 10–20 points. Remove or lazy-load it and re-run before touching anything else.

Should I optimize for mobile or desktop Lighthouse scores?

Mobile. Google's ranking signals come from the mobile version of your page, and Lighthouse's mobile simulation is what you'll be graded against. Desktop scores are always higher and less meaningful for SEO purposes.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

CSS backdrop-filter: blur, brightness, saturate and When to Use EachGoogle Fonts Performance: font-display, preload and next/fontLCP Optimization Guide: Images, Fonts, Server Response and CacheCore Web Vitals in 2026: LCP, CLS, INP — Measure and Fix