Back

Achieving a Perfect 100/100 Lighthouse Score

· 10 min read

Building MyNaijaTax - Nigeria’s privacy-first tax calculator, the team set an ambitious goal to build a useful tool for the average Nigerian. Along the way, the project achieved something remarkable: a perfect 100/100 score across all Lighthouse metrics. Not just performance, but Performance, Accessibility, Best Practices, and SEO, all four categories at 100%.

In this article, you will learn the specific techniques used to hit perfect scores across all four Lighthouse categories, from font optimization and code splitting to semantic HTML and structured data.

Here’s how it was done.

The Result

Perfect 100/100 Lighthouse Score showing Performance, Accessibility, Best Practices, and SEO all at 100
Performance:      100
Accessibility:    100
Best Practices:   100
SEO:              100

View Live Lighthouse Report

The Stack

Before diving into optimizations, here’s what the project was built with:

  • Framework: Next.js 16 (App Router)
  • Styling: Tailwind CSS v4
  • Animations: Framer Motion
  • Deployment: Vercel
  • Analytics: Vercel Analytics

Performance: 100/100

1. Next.js App Router & React Server Components

The project leveraged Next.js 16’s App Router for optimal performance:

// app/layout.tsx - Server Component by default
export const metadata: Metadata = {
  metadataBase: new URL('https://mynaijatax.info'),
  title: 'MyNaijaTax - Free Nigerian Tax Calculator',
  // ...
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
      </head>
      <body className="antialiased">
        <ThemeProvider>
          {children}
        </ThemeProvider>
        <Analytics />
      </body>
    </html>
  );
}

Key optimizations:

  • Server Components for static content
  • Client Components ('use client') only where needed (forms, animations)
  • Automatic code splitting per route

2. Font Optimization

The project used @font-face with optimized loading strategies:

/* globals.css */
@font-face {
  font-family: 'Inter';
  src: url('https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap; /* Critical for LCP */
}

Why this matters:

  • font-display: swap prevents FOIT (Flash of Invisible Text)
  • Preconnect to fonts.gstatic.com in HTML head
  • Variable font reduces file size significantly

3. SVG Over Icon Fonts

Instead of heavy icon libraries, the project used Lucide React (tree-shakeable):

// Only import what is needed
import { Calculator, Building2, ArrowRight } from 'lucide-react';

// Icons render as inline SVG - zero extra HTTP requests
<Calculator className="w-5 h-5 text-white" />

Benefits:

  • No font download for icons
  • Tree-shaking eliminates unused icons
  • Smaller bundle size
  • Better accessibility (semantic SVG)

4. Client-Side Calculations = Zero Backend Latency

All tax calculations happen on the client side:

// lib/tax-calculator.ts
export function calculatePersonalTax(input: PersonalTaxInput): PersonalTaxResult {
  // Pure function - runs instantly client-side
  const taxableIncome = input.grossIncome - totalDeductions - cra;
  const annualTax = calculateProgressiveTax(taxableIncome);

  return {
    annualTax,
    monthlyTax: annualTax / 12,
    netMonthlyIncome: (input.grossIncome - annualTax) / 12,
    // ...
  };
}

Impact:

  • Zero API latency
  • Instant results (useMemo ensures efficient recalculation)
  • Works offline after initial load
  • Privacy-first (no data sent to servers)

5. Image Optimization

We used SVG for the logo instead of raster images:

// No image optimization needed, SVG scales perfectly
<img src="/logo.svg" alt="MyNaijaTax Logo" className="w-10 h-10" />

For social cards, we use Next.js OG Image generation:

// app/api/og/route.tsx
import { ImageResponse } from 'next/og';

export async function GET() {
  return new ImageResponse(
    (
      <div style={{ /* Dynamic OG image */ }}>
        MyNaijaTax
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}

6. CSS Performance

Tailwind CSS v4 with JIT compilation:

// Only the CSS classes we actually use get shipped
<div className="glass-card p-6 rounded-xl backdrop-blur-20">
  {/* Tailwind generates minimal CSS */}
</div>

Custom CSS variables for theming:

:root {
  --bg-primary: #020617;
  --text-primary: #f8fafc;
  --emerald: #10b981;
}

.light {
  --bg-primary: #ffffff;
  --text-primary: #0f172a;
  --emerald: #16a34a;
}

No runtime theme switching overhead - pure CSS variables.

7. Code Splitting & Dynamic Imports

You can use dynamic imports for heavy components:

// Only load ShareModal when user clicks "Share"
const ShareModal = dynamic(() => import('@/components/ShareModal'), {
  loading: () => <div>Loading...</div>,
});

8. Framer Motion Optimization

Animations can kill performance. Here’s how to handle them:

// Using transform properties (GPU-accelerated)
<motion.div
  whileHover={{ scale: 1.02, y: -4 }} // transform
  transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
  {/* Avoid animating: width, height, top, left */}
</motion.div>

Reduced motion support:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Accessibility: 100/100

1. Semantic HTML

// Good semantic structure
<header className="relative z-10">
  <nav aria-label="Main navigation">
    <Link href="/" className="flex items-center gap-2">
      <img src="/logo.svg" alt="MyNaijaTax Logo" className="w-10 h-10" />
      <span className="text-xl font-bold">MyNaijaTax</span>
    </Link>
  </nav>
</header>

<main>
  <h1>Personal Tax Calculator</h1>
  {/* Content */}
</main>

<footer>
  {/* Footer content */}
</footer>

2. Form Accessibility

Every input has proper labels and ARIA attributes:

<div className="w-full">
  <label htmlFor="gross-income" className="block text-sm font-medium mb-2">
    Gross Annual Income
    {required && <span className="text-red-500 ml-1" aria-label="required">*</span>}
  </label>
  <input
    id="gross-income"
    type="text"
    value={displayValue}
    onChange={handleChange}
    placeholder="e.g., 5m or 5,000,000"
    aria-describedby={helpText ? "income-help" : undefined}
    className="naira-input w-full"
    inputMode="decimal" // Mobile-optimized keyboard
  />
  {helpText && (
    <p id="income-help" className="mt-2 text-sm text-slate-500">
      {helpText}
    </p>
  )}
</div>

3. Color Contrast

The project ensured WCAG AAA compliance:

/* All text meets 7:1 contrast ratio */
.light {
  --text-primary: #0f172a;    /* Black on white: 16.1:1 */
  --text-secondary: #475569;  /* Dark gray on white: 9.2:1 */
  --emerald: #16a34a;         /* Green on white: 4.8:1 (AA Large) */
}

Every color combination was tested using Chrome DevTools contrast checker.

4. Focus Indicators

Visible focus states for keyboard navigation:

.btn-primary:focus-visible {
  outline: 2px solid var(--emerald);
  outline-offset: 2px;
}

input:focus-visible {
  border-color: var(--indigo);
  box-shadow: 0 0 0 3px var(--indigo-glow);
}

5. ARIA Labels for Icons

<button
  onClick={toggleTheme}
  className="theme-toggle-btn"
  aria-label="Toggle theme" // Critical for screen readers
>
  {theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
// Hidden but accessible for keyboard users
<a href="#main-content" className="skip-link">
  Skip to main content
</a>

<main id="main-content">
  {/* Content */}
</main>

Best Practices: 100/100

1. HTTPS Everywhere

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

2. No Console Errors

All console warnings were eliminated:

// Before
console.log('Debug info'); // Remove in production

// After
if (process.env.NODE_ENV === 'development') {
  console.log('Debug info');
}

3. Proper Image Alt Text

// Every image has meaningful alt text
<img
  src="/logo.svg"
  alt="MyNaijaTax Logo - Nigerian Tax Calculator"
  className="w-10 h-10"
/>

4. No Deprecated APIs

// Replace deprecated iframe attributes
//  frameBorder, marginHeight, marginWidth
<iframe
  src="https://forms.gle/..."
  width="100%"
  height="1000"
  style={{ border: 'none' }} // Use CSS instead
  title="Feature Request Form" // Required for a11y
/>

5. Browser Compatibility

/* Fallbacks for older browsers */
.glass-card {
  background: rgba(15, 23, 42, 0.6);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px); /* Safari */
}

SEO: 100/100

1. Comprehensive Meta Tags

export const metadata: Metadata = {
  metadataBase: new URL('https://mynaijatax.info'),
  title: 'MyNaijaTax - Free Nigerian Tax Calculator | PAYE & CIT Calculator 2025',
  description: 'Calculate your Nigerian taxes instantly with our free, privacy-first tax calculator. Accurate PAYE and Company Income Tax (CIT) calculations based on 2025 FIRS rates.',
  keywords: 'Nigerian tax calculator, PAYE calculator Nigeria, business tax Nigeria, FIRS tax calculator, income tax Nigeria, tax bands Nigeria, CIT calculator, Nigerian tax rates 2025',

  // Crawling directives
  robots: 'index, follow',

  // Canonical URL
  alternates: {
    canonical: 'https://mynaijatax.info',
  },

  // Open Graph
  openGraph: {
    title: 'MyNaijaTax - Free Nigerian Tax Calculator | PAYE & CIT 2025',
    description: 'Calculate your Nigerian PAYE and Business taxes instantly. 100% private, based on official 2025 FIRS rates.',
    type: 'website',
    locale: 'en_NG',
    url: 'https://mynaijatax.info',
    siteName: 'MyNaijaTax',
    images: [
      {
        url: 'https://mynaijatax.info/api/og',
        width: 1200,
        height: 630,
        alt: 'MyNaijaTax - Nigerian Tax Calculator',
      },
    ],
  },

  // Twitter
  twitter: {
    card: 'summary_large_image',
    title: 'MyNaijaTax - Free Nigerian Tax Calculator',
    description: 'Calculate your Nigerian PAYE and Business taxes instantly.',
    images: ['https://mynaijatax.info/api/og'],
    creator: '@mynaijatax',
  },
};

2. Structured Data (JSON-LD)

// app/layout.tsx
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'WebApplication',
      name: 'MyNaijaTax',
      description: 'Free Nigerian Tax Calculator for PAYE and Business Tax',
      url: 'https://mynaijatax.info',
      applicationCategory: 'FinanceApplication',
      operatingSystem: 'Web Browser',
      offers: {
        '@type': 'Offer',
        price: '0',
        priceCurrency: 'NGN',
      },
      inLanguage: 'en-NG',
      geo: {
        '@type': 'Country',
        name: 'Nigeria',
      },
    }),
  }}
/>

3. Semantic HTML & Headings

// Proper heading hierarchy
<h1>Personal Tax Calculator</h1>
  <h2>Step 1: Income Details</h2>
    <h3>Gross Annual Income</h3>
  <h2>Step 2: Deductions</h2>
    <h3>Pension Contributions</h3>
    <h3>NHF Contributions</h3>

4. Mobile-Friendly Viewport

<meta name="viewport" content="width=device-width, initial-scale=1" />
// Bad
<Link href="/personal">Click here</Link>

// Good
<Link href="/personal">
  Calculate Personal Tax (PAYE)
</Link>

Key Metrics Breakdown

Core Web Vitals

LCP (Largest Contentful Paint):    0.9s  (Target: <2.5s)
FID (First Input Delay):            0ms  (Target: <100ms)
CLS (Cumulative Layout Shift):      0    (Target: <0.1)

How these scores were achieved:

MetricOptimization Strategy
LCP (Largest Contentful Paint)• SVG logo loads instantly
• Font with display: swap
• No above-fold images requiring optimization
FID (First Input Delay)• Minimal JavaScript on initial page load
• React 18 with automatic batching
• Client Components only where needed
CLS (Cumulative Layout Shift)• Fixed sizes for all elements
• No layout shifts from fonts (swap strategy)
• Skeleton states for dynamic content

Other Metrics

FCP (First Contentful Paint):       0.6s
Speed Index:                         0.9s
Time to Interactive:                 0.9s
Total Blocking Time:                 0ms

Lessons Learned

1. Start with Performance in Mind

Don’t retrofit performance - architect for it from day one. The team chose:

  • Next.js App Router for automatic optimizations
  • Client-side calculations (no API latency)
  • Minimal dependencies

2. Test on Real Devices

Lighthouse DevTools is different from PageSpeed Insights. Test both:

  • Desktop throttling
  • Mobile throttling
  • Real mobile devices

3. Accessibility is Non-Negotiable

Every feature should be accessible:

  • Keyboard navigation
  • Screen reader support
  • Color contrast
  • Focus indicators

4. Every KB Matters

You should audit your bundle regularly:

npm run build
# Check .next/analyze output

Current bundle size:

First Load JS:    87.2 kB
Route (app/):
  ├ /             72.1 kB
  ├ /personal     76.4 kB
  └ /business     75.8 kB

5. Dark Mode Done Right

CSS variables + theme class = zero runtime cost:

// No useState for theme colors
// No re-renders on theme change
// Pure CSS switching
document.documentElement.classList.toggle('light');

Tools Used

  1. Lighthouse CI - Automated testing on every deploy
  2. Chrome DevTools - Performance profiling
  3. WebPageTest - Real-world performance testing
  4. Axe DevTools - Accessibility auditing
  5. Vercel Analytics - Real User Metrics (RUM)

The Result: Fast, Accessible, and Private

In this article, you learned how to achieve a perfect 100/100 Lighthouse score by optimizing font loading, using Server Components, writing semantic HTML, ensuring color contrast compliance, implementing structured data, and keeping your JavaScript bundle lean.

MyNaijaTax now serves thousands of Nigerians with:

  • Instant calculations (no backend latency)
  • 100% privacy (client-side processing)
  • Full accessibility (keyboard, screen readers)
  • Mobile-first (PWA-ready)
  • Fast everywhere (sub-second loads globally)

Try It Yourself

Visit mynaijatax.info and share your thoughts.

Questions? Reach out via the feature request form.