Most web performance guides open with Lighthouse scores and Core Web Vitals dashboards — metrics measured on fibre connections in San Francisco. If your users are in Kumasi, Kisumu, or Kigali loading your app over a 2G signal at 50–250 kbps, those benchmarks are nearly useless. You need a different mental model entirely.

Building for low-bandwidth is not about stripping your app down to plain HTML and calling it done. It is about making deliberate architectural choices so that every kilobyte earns its place, and the app remains useful even when the network drops out completely. Here is how to do it.


Understand the Actual Constraint

A 2G EDGE connection delivers roughly 100–250 kbps in real-world conditions, with latency spikes above 500 ms. That means a 1 MB JavaScript bundle takes over 30 seconds to download — before a single line executes. The median mobile web page today ships more than 2 MB of resources. On 2G, that page simply never loads for most users; they abandon it within 10 seconds.

The goal is a meaningful first paint under 5 seconds on a 2G connection, with full interactivity available offline once the app has been visited once.


Start Offline-First, Not Offline-Capable

There is a critical difference between "offline-capable" and "offline-first." Offline-capable apps handle the absence of a network gracefully. Offline-first apps are designed from the ground up to use the network as an enhancement, not a requirement.

The Service Worker API is your foundation. Register a service worker on first load and implement a cache-first strategy for static assets, with a stale-while-revalidate strategy for API responses.

// sw.js — cache-first for static assets
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  // Static shell: always serve from cache
  if (url.pathname.match(/\.(js|css|woff2|svg)$/)) {
    event.respondWith(
      caches.match(event.request).then(
        (cached) => cached || fetch(event.request).then((res) => {
          const clone = res.clone();
          caches.open('static-v1').then((c) => c.put(event.request, clone));
          return res;
        })
      )
    );
    return;
  }

  // API data: stale-while-revalidate
  event.respondWith(
    caches.open('api-v1').then(async (cache) => {
      const cached = await cache.match(event.request);
      const fresh = fetch(event.request).then((res) => {
        cache.put(event.request, res.clone());
        return res;
      });
      return cached || fresh;
    })
  );
});

This pattern means a returning user sees content instantly, even with zero signal. The network fetch happens silently in the background and refreshes the cache for next time.


Aggressively Reduce Your JavaScript Bundle

The single largest culprit in slow load times is JavaScript. Every framework ships with a cost, and on 2G that cost is paid in full, upfront, every time.

Practical steps to cut bundle weight:

  • Audit with source-map-explorer or Bundle Buddy. You will almost always find entire libraries imported for one or two functions. Replace moment.js (67 kB) with date-fns tree-shaken to a few kilobytes. Replace full lodash with native ES methods.
  • Code-split aggressively. Only load the JavaScript needed for the current route. Dynamic import() is universally supported and integrates cleanly with React, Vue, and Svelte routers.
  • Defer non-critical scripts. Analytics, chat widgets, and social embeds should load after the user interacts with the page, not before.
  • Target a total JS budget of under 100 kB (compressed). This is achievable for most CRUD-style SaaS products.

SVGs Over Raster Images — Always

Images are typically 40–60% of page weight. On low-bandwidth connections, a 200 kB hero image is a death sentence for your load time.

Adopt these rules without exception:

  • Use SVG for all icons, logos, and illustrations. A detailed SVG icon is often 1–3 kB. A PNG equivalent is 15–40 kB.
  • Lazy-load all raster images using the native loading="lazy" attribute. Images below the fold should never block the initial render.
  • Serve modern formats. WebP delivers 25–35% smaller files than JPEG at equivalent quality. AVIF goes even further. Use a <picture> element with appropriate fallbacks.
  • Set explicit width and height attributes on every image to prevent layout shift, which wastes re-render cycles on slower devices.

Where possible, replace decorative photography with SVG illustrations entirely. This is not a design compromise — it is a product decision that dramatically improves perceived quality on budget Android handsets.


Build a Lightweight App Shell

The App Shell pattern separates your UI chrome (navigation, layout, fonts) from your data. The shell is cached on first install and loads instantly on every subsequent visit, even offline. Only the dynamic content fetches from the network.

Keep the shell under 30 kB total — HTML, CSS, and critical JS combined. Use system fonts (font-family: system-ui, sans-serif) for the shell to eliminate font download entirely. If a custom brand font is required, subset it to the characters you actually use, reducing a 120 kB WOFF2 to under 10 kB.


Design for Intermittent Connectivity — Not Just Slow Connectivity

2G networks in practice are not just slow — they drop out unpredictably. Design your data layer to handle this:

  • Queue writes locally and sync when reconnected. Use IndexedDB with a background sync service worker to hold form submissions and mutations until the connection returns. Users should never lose data because they hit "Submit" in a dead zone.
  • Show optimistic UI. Update the local UI immediately on user action, then reconcile with the server. This eliminates the perceived latency of slow writes.
  • Use the Network Information API to detect connection type and conditionally serve lower-resolution assets or disable auto-playing media on effectiveType === '2g'.

Test Like Your Users Test

No amount of theoretical optimisation replaces testing on actual conditions. Chrome DevTools allows you to throttle the network to 2G in the Network panel. Go further: use a real Android device in the GHS 400–600 price range (the dominant market segment in West Africa), enable 2G simulation in developer options, and navigate your app as a first-time user.

You will find at least three things that are unacceptably slow. Fix those first.


Why This Matters for Your Project

If you are building a SaaS product, a fintech platform, or a business tool with any ambition to serve African markets, low-bandwidth performance is not a nice-to-have — it is a market access requirement. The users who stand to gain the most from your software are often the ones with the least forgiving connections. Offline-first architecture, lean bundles, and SVG-based UI are not technical luxuries; they are the engineering decisions that determine whether your product reaches the people it is built for.