How We Achieved a Perfect 100/100 Lighthouse Score on MyNaijaTax
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
Performance: 100
Accessibility: 100
Best Practices: 100
SEO: 100
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: 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, 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>
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
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" />
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 we achieved this:
| 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. 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
- 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
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.
Enjoyed this post? Check out more articles on my blog.
View all posts
Comments