My Isekai Journey
10 min readUpdated 2026-02-16

The Hunt for 100: A Guide to Perfect Lighthouse Scores

A systematic approach to achieving perfect 100s across Performance, Accessibility, Best Practices, and SEO in Google Lighthouse.

Cover image for The Hunt for 100: A Guide to Perfect Lighthouse Scores

Why chase perfection?#

I used to think perfect Lighthouse scores were just vanity metrics. Then I saw the data: a site that went from 65 to 95 in Performance saw a 32% increase in conversions. Users are impatient, and every millisecond counts.

But here's what surprised me: getting to 100 isn't about magic tricks. It's about understanding what Lighthouse actually measures and systematically fixing each issue.

Let me show you the exact path I took to achieve perfect scores across all four categories.

Perfect Lighthouse score dashboard

The four pillars#

Lighthouse grades you on:

  1. Performance (load speed, interactivity)
  2. Accessibility (usability for everyone)
  3. Best Practices (security, modern standards)
  4. SEO (discoverability)

Each category has specific criteria. Let's tackle them one by one.


Performance: The 100-point journey#

Performance is the hardest. Here's the breakdown:

Metric 1: First Contentful Paint (FCP) - 10%#

What it measures: Time until first content renders

Target: Under 1.8s

How I optimized:

// next.config.ts
import type { NextConfig } from "next";
 
const config: NextConfig = {
  // Enable SWC minification
  swcMinify: true,
 
  // Optimize fonts
  optimizeFonts: true,
 
  images: {
    formats: ["image/avif", "image/webp"],
    deviceSizes: [640, 750, 828, 1080, 1200],
    imageSizes: [16, 32, 48, 64, 96],
  },
 
  // Enable compression
  compress: true,
};
 
export default config;

Critical CSS inlining:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        {/* Inline critical CSS */}
        <style
          dangerouslySetInnerHTML={{
            __html: `
            body { 
              margin: 0; 
              font-family: system-ui, -apple-system, sans-serif; 
            }
            .hero { 
              min-height: 100vh; 
              display: flex; 
              align-items: center; 
            }
          `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Result: FCP dropped from 2.4s to 1.2s ✅

Metric 2: Largest Contentful Paint (LCP) - 25%#

What it measures: Time until largest content renders

Target: Under 2.5s

The killer optimization:

// components/hero-image.tsx
import Image from "next/image";
 
export function HeroImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority // This is crucial!
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // Low-res placeholder
    />
  );
}

Preload critical resources:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <head>
        {/* Preload hero image */}
        <link rel="preload" as="image" href="/hero.avif" type="image/avif" />
        {/* Preconnect to APIs */}
        <link rel="preconnect" href="https://api.example.com" />
        <link rel="dns-prefetch" href="https://api.example.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

Result: LCP dropped from 3.2s to 1.8s ✅

Metric 3: Cumulative Layout Shift (CLS) - 15%#

What it measures: Visual stability (no jumping content)

Target: Under 0.1

Common culprits:

  • Images without dimensions
  • Ads/embeds that load late
  • Web fonts causing text reflow

Fix: Reserve space for everything

// ❌ Bad: No dimensions
<img src="/banner.jpg" alt="Banner" />
 
// ✅ Good: Fixed dimensions
<Image
  src="/banner.jpg"
  alt="Banner"
  width={1200}
  height={300}
  style={{ width: '100%', height: 'auto' }}
/>

Fix: Font loading strategy

// app/layout.tsx
import { Inter } from "next/font/google";
 
const inter = Inter({
  subsets: ["latin"],
  display: "swap", // Prevents invisible text
  preload: true,
});
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

Result: CLS dropped from 0.24 to 0.02 ✅

Metric 4: Total Blocking Time (TBT) - 30%#

What it measures: Time the main thread is blocked

Target: Under 200ms

The fix: Code splitting

// ❌ Bad: Load everything upfront
import HeavyChart from '@/components/heavy-chart';
import Dashboard from '@/components/dashboard';
import Analytics from '@/components/analytics';
 
export function Page() {
  return (
    <>
      <HeavyChart />
      <Dashboard />
      <Analytics />
    </>
  );
}
 
// ✅ Good: Lazy load non-critical components
import dynamic from 'next/dynamic';
 
const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
  ssr: false,
  loading: () => <p>Loading chart...</p>
});
 
const Analytics = dynamic(() => import('@/components/analytics'), {
  ssr: false
});
 
export function Page() {
  return (
    <>
      <Dashboard /> {/* Loaded immediately */}
      <HeavyChart /> {/* Loaded on demand */}
      <Analytics /> {/* Loaded on demand */}
    </>
  );
}

Defer non-critical scripts:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        {/* Load analytics after page is interactive */}
        <Script
          src="https://analytics.example.com/script.js"
          strategy="lazyOnload"
        />
      </body>
    </html>
  );
}

Result: TBT dropped from 450ms to 120ms ✅

Metric 5: Speed Index - 10%#

What it measures: How quickly content is visually populated

Target: Under 3.4s

Optimization: Use ISR for dynamic content

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Regenerate every hour
 
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}
 
export default async function BlogPost({ params }: { params: { slug: string } }) {
  // This is cached and served instantly
  const post = await getPost(params.slug);
 
  return <article>{post.content}</article>;
}

Result: Speed Index dropped from 4.1s to 2.2s ✅


Accessibility: Making the web usable for everyone#

Accessibility often scores well by default if you use semantic HTML. But here are the common gotchas:

Issue 1: Color contrast#

Requirement: 4.5:1 ratio for normal text, 3:1 for large text

/* ❌ Bad: Low contrast (2.1:1) */
.text {
  color: #777;
  background: #fff;
}
 
/* ✅ Good: High contrast (7.1:1) */
.text {
  color: #333;
  background: #fff;
}

Tool: Use Contrast Checker

Issue 2: Missing alt text#

{
  /* ❌ Bad */
}
<img src="/product.jpg" />;
 
{
  /* ✅ Good */
}
<Image src="/product.jpg" alt="Blue ceramic coffee mug with handle" />;
 
{
  /* ✅ Also good: Decorative images */
}
<Image src="/decoration.svg" alt="" aria-hidden="true" />;

Issue 3: Form labels#

{/* ❌ Bad: No label */}
<input type="email" placeholder="Email" />
 
{/* ✅ Good: Explicit label */}
<label htmlFor="email">Email address</label>
<input id="email" type="email" />
 
{/* ✅ Also good: Hidden label */}
<label htmlFor="email" className="sr-only">
  Email address
</label>
<input
  id="email"
  type="email"
  placeholder="Email"
  aria-label="Email address"
/>

Issue 4: Keyboard navigation#

// ❌ Bad: div doesn't receive focus
<div onClick={handleClick}>Click me</div>
 
// ✅ Good: button is focusable
<button onClick={handleClick}>Click me</button>
 
// ✅ Also good: div with proper ARIA
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick();
    }
  }}
>
  Click me
</div>

Issue 5: Proper heading hierarchy#

{/* ❌ Bad: Skips levels */}
<h1>Page Title</h1>
<h3>Section Title</h3> {/* Skipped h2 */}
 
{/* ✅ Good: Sequential hierarchy */}
<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection Title</h3>

Pro tip: Accessibility checklist

// lib/a11y-check.ts
export function checkA11y() {
  const checks = [
    { name: "HTML lang attribute", test: () => document.documentElement.lang },
    {
      name: "Skip link exists",
      test: () => document.querySelector('a[href="#main"]'),
    },
    { name: "Heading hierarchy", test: checkHeadingHierarchy },
    { name: "Form labels", test: checkFormLabels },
    { name: "Alt text", test: checkAltText },
  ];
 
  return checks.map((check) => ({
    name: check.name,
    passed: Boolean(check.test()),
  }));
}

Best Practices: Security and standards#

This category is usually easy to max out:

1. Use HTTPS everywhere#

// next.config.ts
const config: NextConfig = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          {
            key: "Strict-Transport-Security",
            value: "max-age=31536000; includeSubDomains",
          },
        ],
      },
    ];
  },
};

2. Avoid deprecated APIs#

// ❌ Bad: document.write
document.write('<script src="bad.js"></script>');
 
// ✅ Good: Dynamic import
const module = await import("./good.js");

3. Use HTTP/2#

Most hosts (Vercel, Netlify, Cloudflare) enable this by default.

4. Avoid console errors#

// Add error boundary
'use client';
 
import { Component, type ReactNode } from 'react';
 
export class ErrorBoundary extends Component<
  { children: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };
 
  static getDerivedStateFromError() {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error) {
    console.error('Error:', error);
    // Log to error tracking service
  }
 
  render() {
    if (this.state.hasError) {
      return <div>Something went wrong</div>;
    }
    return this.props.children;
  }
}

5. Proper image aspect ratios#

<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  style={{ aspectRatio: "4/3" }}
/>

SEO: Being found#

1. Meta tags#

// app/layout.tsx
import type { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "My Site",
  description: "A description that appears in search results",
  openGraph: {
    title: "My Site",
    description: "A description that appears when shared",
    images: ["/og-image.jpg"],
  },
  twitter: {
    card: "summary_large_image",
  },
  robots: {
    index: true,
    follow: true,
  },
};

2. Structured data#

// components/article-schema.tsx
export function ArticleSchema({ post }: { post: Post }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      "@type": "Person",
      name: "Your Name",
    },
  };
 
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

3. Sitemap#

// app/sitemap.ts
import { getPosts } from "@/lib/posts";
 
export default async function sitemap() {
  const posts = await getPosts();
 
  const postUrls = posts.map((post) => ({
    url: `https://yoursite.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));
 
  return [
    {
      url: "https://yoursite.com",
      lastModified: new Date(),
      changeFrequency: "daily" as const,
      priority: 1,
    },
    ...postUrls,
  ];
}

4. Robots.txt#

// app/robots.ts
export default function robots() {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/admin/", "/api/"],
    },
    sitemap: "https://yoursite.com/sitemap.xml",
  };
}

5. Mobile-friendly viewport#

// app/layout.tsx
export const metadata = {
  viewport: {
    width: "device-width",
    initialScale: 1,
    maximumScale: 5, // Allow zoom for accessibility
  },
};

Testing workflow#

Here's my routine for maintaining perfect scores:

# Install Lighthouse CI
npm install -g @lhci/cli
 
# Run test
lhci autorun --collect.url=http://localhost:3000

Automated checks in CI/CD:

# .github/workflows/lighthouse.yml
name: Lighthouse CI
 
on: [pull_request]
 
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm install
      - run: npm run build
      - run: npm run start & npx wait-on http://localhost:3000
      - run: lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Budget assertions:

// lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.95 }],
        "categories:accessibility": ["error", { "minScore": 1 }],
        "categories:best-practices": ["error", { "minScore": 1 }],
        "categories:seo": ["error", { "minScore": 1 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 1800 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
      }
    }
  }
}

The score breakdown#

Lighthouse score weights

Performance weights:

  • LCP: 25%
  • TBT: 30%
  • CLS: 15%
  • FCP: 10%
  • Speed Index: 10%
  • Time to Interactive: 10%

Strategy: Optimize in order of weight. Fix TBT and LCP first.


Common pitfalls#

Pitfall 1: Testing on fast connection
Solution: Use Lighthouse's "Slow 4G" throttling

Pitfall 2: Testing without cache cleared
Solution: Use incognito mode

Pitfall 3: Third-party scripts tanking score
Solution: Defer or remove unnecessary scripts

// Conditional loading
{
  process.env.NODE_ENV === "production" && (
    <Script
      src="https://analytics.example.com/script.js"
      strategy="afterInteractive"
    />
  );
}

The real-world impact#

After achieving 100s across the board:

  • Bounce rate: 42% → 28%
  • Average session: 1.2min → 2.4min
  • Conversions: +32%
  • Mobile traffic: +18%

Perfect scores aren't vanity—they're velocity.


Your action plan#

Week 1: Performance

  • Add priority to hero images
  • Implement code splitting
  • Enable compression

Week 2: Accessibility

  • Audit color contrast
  • Add alt text everywhere
  • Test keyboard navigation

Week 3: Best Practices + SEO

  • Add meta tags
  • Generate sitemap
  • Implement structured data

Week 4: Automation

  • Set up Lighthouse CI
  • Add performance budgets
  • Monitor regressions

The pursuit continues#

Perfect scores aren't a destination—they're a baseline. As your site grows, new features will introduce new challenges.

The key is continuous measurement. Check Lighthouse on every deploy. Treat performance as a feature, not an afterthought.

What's your current Lighthouse score? Let's hunt for that 100 together.