What Your Website Costs to Open in Lagos
An average home page costs about ₦2 to open on Nigerian mobile data. How to measure your site's cost in naira and make Next.js pages lighter.
CarnielOn this page13 sections
Every time someone opens your website on mobile data, they pay for it. Not you. Them.
On MTN's cheapest 30-day bundle in Nigeria, 2 GB for ₦1,500, a gigabyte costs ₦750 (Mobility Arena, 2026 data plans). The median mobile home page now weighs 2,559 KB, according to the 2025 Web Almanac (HTTP Archive). Put those together and an ordinary home page costs about ₦2 to open.
₦2 sounds like nothing. It isn't, once you do the rest of the maths.
The maths nobody does
One reader, one month. Ten average pages a day for 30 days is about 770 MB. That's ₦575, or almost 40% of that ₦1,500 bundle, spent loading websites like the ones we build.
Against a real income. Nigeria's national minimum wage is ₦70,000 a month (Nairametrics). The bundle alone is just over 2% of that.
Your traffic, your users' bill. If your site gets 1,000 visits a day, your visitors are paying about ₦1,900 a day, or ₦700,000 a year, just to open the home page.
And the median is getting heavier. The same Web Almanac report found the median mobile home page grew by more than 200% between 2015 and 2025. The two biggest parts of it are images (911 KB) and JavaScript (646 KB).
When a page is heavy, people on prepaid data don't complain. They close the tab before it finishes loading, and you never find out why your bounce rate is high.
Why we don't feel it
Most of us build on office fibre or a big monthly plan, on a laptop that's newer than most of our users' phones. A 4 MB page loads in a second and feels fine. The same page on a mid-range Android, on a weak signal in traffic on Third Mainland Bridge, takes long enough that the user gives up. And it still costs them the data for the parts that did load.
The fix starts with seeing the problem the way your users do.
Step 1: find out what your page costs
Chrome DevTools will show you the transferred size at the bottom of the Network tab, and you can switch the throttling dropdown to "Slow 4G" to feel the load time. But a number in naira is easier to argue with in a team meeting than a number in kilobytes, so here's a small script that gives you one:
// page-cost.mjs
// Usage: node page-cost.mjs https://your-site.com 750
// The second argument is what your users pay per GB, in naira.
import { chromium } from 'playwright'
const url = process.argv[2]
const nairaPerGB = Number(process.argv[3] ?? 750)
const browser = await chromium.launch()
const page = await browser.newPage()
const cdp = await page.context().newCDPSession(page)
await cdp.send('Network.enable')
let bytes = 0
cdp.on('Network.loadingFinished', (e) => {
bytes += e.encodedDataLength // bytes over the wire, after compression
})
await page.goto(url, { waitUntil: 'networkidle' })
await page.waitForTimeout(2000) // let late requests finish
await browser.close()
const mb = bytes / 1_000_000
const cost = (mb / 1000) * nairaPerGB
console.log(`${url}`)
console.log(`Transferred: ${mb.toFixed(2)} MB`)
console.log(`Cost per visit: ₦${cost.toFixed(2)}`)
console.log(`Cost for 1,000 visits: ₦${Math.round(cost * 1000).toLocaleString()}`)Install Playwright with npm i -D playwright and npx playwright install chromium, then point it at your site. It counts the actual bytes sent over the network, after compression, which is what your user's data bundle is charged for. I tested it against a page with a known 1 MB image and 600 KB script, and it reported 1.65 MB every run.
Run it on your home page, your product page and your heaviest dashboard. The results are usually a surprise.
Step 2: find what's heavy
For JavaScript, Next.js 16.1 added an experimental bundle analyzer that works with Turbopack (Next.js 16.1):
next experimental-analyzeIt opens an interactive view of your production bundles. You can filter by route and, most usefully, see the import chain that explains why a module ended up in the client bundle at all. That's how you find the date library a single component pulled in, or the icon pack that shipped all 1,500 icons.
For everything else, sort the DevTools Network tab by size. Images and fonts are usually at the top.
Step 3: the fixes that save the most
Keep JavaScript on the server
In the App Router, components are Server Components unless you add 'use client'. Server Components send HTML, not JavaScript. The most common way Next.js apps get heavy is a 'use client' at the top of a big layout or page, which drags everything it imports into the browser bundle.
Push 'use client' down to the smallest piece that actually needs state or a click handler. A product page can be fully server-rendered, with only the "Add to cart" button as a client component.
Let next/image do its job
next/image resizes images and serves modern formats, but only if you tell it how big the image will be on screen. Without sizes, a phone can end up downloading an image sized for a desktop.
<Image
src="/hero.jpg"
alt="Market stall in Balogun with bright umbrellas"
width={1600}
height={900}
sizes="(max-width: 768px) 100vw, 50vw"
priority
/>Then turn on AVIF, which is usually noticeably smaller than WebP for photos:
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
},
}
export default configUse fewer fonts
Every font weight is another file. next/font self-hosts and subsets fonts for you, but it can't stop you loading six weights you barely use. Pick one family, two weights, and the Latin subset:
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], weight: ['400', '600'] })Question every third-party script
Chat widgets, heatmaps and marketing pixels are often heavier than your whole app. For each one, ask whether anyone has looked at its data in the last month. If it stays, load it after the page is usable:
import Script from 'next/script'
<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />Make the second visit free
Next.js already serves the files in /_next/static with long-lived, immutable cache headers, because their names change when their content does. That means a returning visitor doesn't download your JavaScript again. Make sure nothing in your setup, like a CDN rule or a custom header, overrides that.
Step 4: respect users who ask to save data
Chromium-based browsers send a Save-Data: on header when the user has turned on a data-saving mode (http.dev). It's a direct request from the user, and in a Server Component you can read it and send a lighter page:
import { headers } from 'next/headers'
export default async function HomePage() {
const saveData = (await headers()).get('save-data') === 'on'
return (
<main>
{saveData ? <HeroImageSmall /> : <HeroVideo />}
{/* ...rest of the page */}
</main>
)
}Don't rely on it as your only defence, since many users never turn it on. Treat it as a bonus on top of a page that's already light.
Step 5: set a budget and enforce it
Performance fixes don't last unless something stops the page getting heavy again. Turn the cost script into a check by adding two lines at the end:
const budget = Number(process.env.NAIRA_BUDGET ?? 1)
if (cost > budget) {
console.error(`Over budget: ₦${cost.toFixed(2)} > ₦${budget}`)
process.exit(1)
}Run it against your preview deployment in CI. Now "this page costs more than ₦1 to open" fails the build the same way a broken test does, and the conversation about that new carousel happens before it ships, not after.
A target worth aiming for
Half the median is a good first goal: about 1.3 MB, or ₦1 per visit on that MTN bundle. It's very achievable for most content sites and stores once images and client JavaScript are under control.
None of this is only about Nigeria. Prepaid data is the norm across much of the world, and a lighter page is faster for everyone, including users on fibre. But if you build for Lagos, you're building for people who see the cost of your website on their phone balance. Build like it.