The App That's Fast on Your WiFi and Dead on Their 3G
Here's the trap: you build a web app, test it on office WiFi, and it loads in a couple of seconds. Ship it. Then your users open it on 3G in Mbare (a high-density suburb in Harare) and stare at a blank screen until they give up and leave.
An app that feels instant on WiFi can take the better part of a minute on a congested 3G connection. Users don't wait for that. Building for African users means treating the slow-network experience as the primary experience, not an edge case.
The African mobile context (that you can't ignore):
- 3G networks: Still dominant in rural areas. 4G is spotty in cities. 5G is luxury.
- Expensive data: Data is expensive relative to income across the region. Users notice every MB.
- Low-end devices: Entry-level Android phones with limited RAM and slow processors. Your fancy animations will choke these devices.
- Limited storage: Users uninstall apps aggressively to save space. PWAs are a lifeline.
If your web app isn't optimized for mobile, you're losing most of your potential users. Here's how to fix that.
Core Principles for Mobile-First Web Apps
1. Optimize for Data Usage
Every MB counts. Users will abandon your app if it consumes too much data.
Image Optimization:
// Use modern formats (WebP, AVIF)
<Image
src="/product.jpg"
alt="Product"
width={400}
height={300}
loading="lazy"
quality={75}
formats={['webp', 'jpg']}
/>
// Serve responsive images
<img
srcset="
/product-400.webp 400w,
/product-800.webp 800w,
/product-1200.webp 1200w
"
sizes="(max-width: 768px) 100vw, 50vw"
src="/product-800.webp"
alt="Product"
/>
Result: WebP typically produces dramatically smaller files than JPEG at comparable quality, and responsive sizing means phones never download desktop-sized images.
Code Splitting:
// Next.js dynamic imports
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <Spinner />,
ssr: false, // Don't render on server
});
// Only load chart code when user views the page
function Analytics() {
return <HeavyChart data={data} />;
}
Result: Initial bundle size drops substantially. Users download the chart library only if they visit the analytics page.
2. Build for Slow Networks
Test on Real Networks:
Chrome DevTools Network Throttling is a start, but test on actual 3G networks and real low-end devices, not just emulators on a fast connection.
Optimistic UI Updates:
// Don't wait for server response
function likePost(postId) {
// Optimistically update UI
setLikes(prev => prev + 1);
setLiked(true);
// Send request in background
api.likePost(postId).catch(() => {
// Rollback on error
setLikes(prev => prev - 1);
setLiked(false);
toast.error('Like failed. Try again.');
});
}
Result: App feels instant even on 3G. Users don't wait for server round-trips.
Service Workers for Offline Caching:
// Cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'/app.js',
'/logo.png',
]);
})
);
});
// Serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Result: Repeat visits load instantly from cache. Works offline.
3. Reduce JavaScript Execution Time
Low-end phones have slow CPUs. Heavy JavaScript causes jank and freezes.
Use Server-Side Rendering (SSR):
// Next.js SSR
export async function getServerSideProps() {
const products = await fetchProducts();
return { props: { products } };
}
export default function Shop({ products }) {
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
Result: HTML arrives pre-rendered. No client-side data fetching. Faster Time to Interactive (TTI).
Lazy Load Components:
// Load modal only when user clicks button
const [showModal, setShowModal] = useState(false);
const Modal = dynamic(() => import('@/components/Modal'));
return (
<>
<Button onClick={() => setShowModal(true)}>Open</Button>
{showModal && <Modal onClose={() => setShowModal(false)} />}
</>
);
4. Design for Touch & Small Screens
Touch Targets: Minimum 44x44px (Apple's guideline). We use 48x48px for comfort.
Bottom Navigation: Place primary actions at bottom of screen (easy thumb reach). Top navs are hard to reach on 6" phones.
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t">
<div className="flex justify-around py-2">
<NavButton icon={Home} label="Home" />
<NavButton icon={Search} label="Search" />
<NavButton icon={Cart} label="Cart" />
<NavButton icon={Profile} label="Profile" />
</div>
</nav>
Performance Budgets for African Markets
The budgets we work toward on projects for African markets:
| Metric | Target (3G) | Max Allowed |
|---|---|---|
| First Contentful Paint | < 2s | 3s |
| Time to Interactive | < 4s | 6s |
| Total Bundle Size | < 200 KB | 350 KB |
| Page Weight (initial load) | < 1 MB | 2 MB |
How we enforce this:
// Fail CI build if bundle exceeds budget
// next.config.js
module.exports = {
performance: {
maxAssetSize: 350000, // 350 KB
maxEntrypointSize: 350000,
},
};
Progressive Web App (PWA) Checklist
PWAs eliminate app store friction and reduce install size vs native apps. Every web app we build is a PWA.
1. Add Web App Manifest:
// public/manifest.json
{
"name": "MyStore",
"short_name": "MyStore",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#011f26",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
2. Enable Service Worker:
Use next-pwa for Next.js or Workbox for vanilla React.
3. Add Install Prompt:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallBanner();
});
function installApp() {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choice) => {
if (choice.outcome === 'accepted') {
console.log('App installed');
}
});
}
Tools We Use for Mobile-First Development
- Lighthouse CI: Automated performance audits on every PR
- WebPageTest: Test on real 3G networks in African cities
- BundlePhobia: Check npm package sizes before installing
- Next.js Image Optimization: Automatic WebP conversion + responsive images
- Sentry: Monitor real-user performance metrics (FCP, TTI, CLS)
Mistakes to Avoid (We've Made Most of Them)
- Using heavy UI libraries: Big component libraries can add hundreds of KB to your bundle before you've written a line of product code. Utility-first CSS like Tailwind ships no runtime. Pick lightweight tools for African markets.
- Not testing on real devices: Emulators lie. Throttled DevTools on a MacBook is not a low-end Android phone on a real network. Test on the devices your users actually own before every launch.
- Ignoring 3G performance: Lighthouse defaults to simulated 4G. Run your audits on "Slow 3G" mode. If it passes there, it'll work anywhere.
- Loading analytics eagerly: Analytics scripts add weight and can block rendering. Lazy-load them after Time to Interactive. Users matter more than metrics.
The Bottom Line
Mobile-first isn't just responsive design. It's performance optimization, data efficiency, offline support, and touch-optimized UX. It's testing on entry-level Android phones in Mbare, not MacBook Pros in Silicon Valley. It's building for 3G, not WiFi.
Get these right, and you'll build web apps that work for the hundreds of millions of Africans who reach the internet primarily through a phone.
We build every web app mobile-first for African markets, from e-commerce platforms to healthcare dashboards. If you're building for African users and want to avoid the mistakes we made, let's talk.
Tech Stack: Next.js, Tailwind CSS, next-pwa, Sharp (image optimization), Lighthouse CI, Sentry, WebPageTest