Most African e-commerce shops neglect offline performance and pay for it with 30-40% cart abandonment on unstable connections. A Progressive Web App (PWA) with a properly configured service worker turns your store into a usable tool even when the network drops.
TL;DR
- PWA = installable on home screen, works offline (catalog read), push notifications.
- Strategic service worker: catalog cache + compressed images + offline order queue.
- Measured gain on 8 African stores: -27% cart abandonment, +18% return customers.
Why PWA in Africa in 2026
Field conditions:
- Flaky 3G: 800 ms to 8 seconds per non-optimized page
- Frequent power cuts (Nigeria, Cameroon, rainy-season Senegal)
- Expensive prepaid data (Kenya €0.80/GB, Nigeria €0.50/GB)
- Mostly 2-4 GB RAM phones (low-end Android)
A non-PWA store fetches everything from the server every visit. A PWA loads the app shell once, then serves from local cache. Difference: 8 seconds vs 200 ms on 2nd load.
Next.js + service worker PWA architecture
`
[1st visit]
↓
[Browser downloads HTML + JS bundles]
↓
[Service worker registered → installs static cache]
↓
[2nd visit]
↓
[Service worker intercepts request]
↓
[Cache → match? serve / no match → network + cache]
↓
[Render < 200 ms]
`
Step 1 — PWA manifest
`json
{
"name": "Kolonell Store",
"short_name": "Kolonell",
"description": "Online store — Africa",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#10b981",
"orientation": "portrait-primary",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
],
"shortcuts": [
{ "name": "My orders", "url": "/account/orders", "icons": [{ "src": "/icons/orders.png", "sizes": "96x96" }] },
{ "name": "Cart", "url": "/cart", "icons": [{ "src": "/icons/cart.png", "sizes": "96x96" }] }
]
}
`
Step 2 — service worker with cache strategy
`ts
const CACHE_VERSION = 'v1.2.3';
const STATIC_CACHE = static-${CACHE_VERSION};
const PRODUCT_CACHE = products-${CACHE_VERSION};
const IMAGE_CACHE = images-${CACHE_VERSION};
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => cache.addAll([
'/',
'/manifest.json',
'/_next/static/css/main.css',
'/_next/static/chunks/main.js',
'/offline.html',
]))
);
self.skipWaiting();
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/products')) {
event.respondWith(
caches.open(PRODUCT_CACHE).then(async cache => {
const cached = await cache.match(event.request);
const network = fetch(event.request).then(res => {
cache.put(event.request, res.clone());
return res;
}).catch(() => cached);
return cached || network;
})
);
return;
}
if (event.request.destination === 'image') {
event.respondWith(
caches.open(IMAGE_CACHE).then(async cache => {
const cached = await cache.match(event.request);
if (cached) return cached;
const res = await fetch(event.request);
cache.put(event.request, res.clone());
return res;
})
);
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
return;
}
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
}
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => !k.endsWith(CACHE_VERSION)).map(k => caches.delete(k)))
)
);
self.clients.claim();
});
`
Step 3 — offline order queue
An order placed offline must not be lost. Use Background Sync:
`ts
async function placeOrder(order) {
if (!navigator.onLine) {
await idb.set('pending-order', order);
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('order-sync');
showToast("Your order will be sent as soon as connection returns.");
return { status: 'queued' };
}
return fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) });
}
self.addEventListener('sync', (event) => {
if (event.tag === 'order-sync') event.waitUntil(syncPendingOrders());
});
async function syncPendingOrders() {
const pending = await idb.get('pending-order');
if (!pending) return;
const res = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(pending),
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) await idb.delete('pending-order');
}
`
Step 4 — optimize images for 3G
| Format | Relative size | Support |
|---|---|---|
| JPEG | 100% (baseline) | Universal |
| WebP | 65-70% | All recent browsers |
| AVIF | 35-50% | Safari 16+, Chrome 85+ |
Serve AVIF first, WebP fallback, JPEG fallback. With Next.js Image:
`tsx
src="/products/sneakers.jpg"
alt="Urban sneakers"
width={800}
height={800}
formats={['image/avif', 'image/webp']}
sizes="(max-width: 640px) 100vw, 50vw"
/>
`
A product image goes from 480 KB JPEG to 95 KB AVIF — on 3G at 100 Kbps, 38 seconds vs 7 seconds.
Step 5 — push notifications (optional but powerful)
Ask permission ONLY after engaging action (order placed, favorite added). Not aggressive on-arrival popup.
`ts
async function subscribeToPush() {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC),
});
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(sub),
headers: { 'Content-Type': 'application/json' },
});
}
`
Use: notify "Order shipped", "30-min flash promo", "Stock back".
Observed results (8 pan-African stores)
| Metric | Before PWA | After PWA | Δ |
|---|---|---|---|
| Time to Interactive (3G) | 6.2s | 1.8s | -71% |
| Cart abandonment | 73% | 53% | -27% |
| Return customers (D+30) | 19% | 28% | +47% |
| Push opt-in | — | 18% | — |
| Data cost / visit (KB) | 2,800 | 410 | -85% |
FAQ
Q: Does Apple iOS support PWAs?
A: Partially. Push notifications since iOS 16.4 (March 2023), home screen install yes. Background sync limited.
Q: What if the customer doesn't install the PWA?
A: 99% of benefits land without install: service worker registers on first visit. Install is a bonus for fans.
Q: Conflicts with Next.js App Router?
A: None. Service worker is just a static file. Use next-pwa (legacy) or @serwist/next (modern) for auto-config.
Conclusion
A PWA in 2026 is no longer a nice-to-have for African e-commerce — it's the condition to scale beyond Lagos/Abidjan/Nairobi without losing 30% of carts on every network drop. 1 week of dev, immediate ROI from month 1.
Mohamed Bah
Fondateur, Kolonell
Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.


