Back to blog

How We Achieved a Perfect 100/100 Lighthouse Score on MyNaijaTax

10 min read
performance lighthouse nextjs optimization web-vitals
Share this post: Twitter LinkedIn

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

Here’s how we did it.

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 we 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

We 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

We 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, we 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

We used 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. We solved for this by:

// 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

We 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) */
}

We tested every color combination 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

We eliminated all console warnings:

// 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

// We replaced 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 we achieved this:

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. We 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

We audit our 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 We 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

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 let us know your thoughts.

Questions? Reach out to us via the feature request form.


Abisoye Alli-Balogun

Written by Abisoye Alli-Balogun

Full Stack Product Engineer building scalable distributed systems and high-performance applications. Passionate about microservices, cloud architecture, and creating delightful user experiences.

Enjoyed this post? Check out more articles on my blog.

View all posts

Comments