Achieving a Perfect 100/100 Lighthouse Score
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
Performance: 100
Accessibility: 100
Best Practices: 100
SEO: 100
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: swapprevents FOIT (Flash of Invisible Text)- Preconnect to
fonts.gstatic.comin 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>
6. Skip Links
// 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" />
5. Descriptive Link Text
// 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:
| Metric | Optimization 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
- Lighthouse CI - Automated testing on every deploy
- Chrome DevTools - Performance profiling
- WebPageTest - Real-world performance testing
- Axe DevTools - Accessibility auditing
- 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.