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