DEV Community

Cover image for Optimizing for a 120 kbps Network That Drops: Do Less, Cache with Boundaries, Survive Offline
Armando
Armando

Posted on

Optimizing for a 120 kbps Network That Drops: Do Less, Cache with Boundaries, Survive Offline

Reality check first: median fixed-connection download in Cuba is 3.48 Mbps vs 104.43 Mbps worldwide (Ookla via DataReportal), and the working condition is closer to 120 kbps, dropping at any moment.

The wrong question is "how do I make my app faster?". The right one:

How much work does my app actually need?

Optimization starts by not doing: not downloading, not computing, not requesting, not rendering, not sending what isn't necessary.

Rule 1 — eliminate before compressing

Data that is never sent beats data that is compressed.

Rule 2 — an API is a resource; return only what's used

Every request costs: DNS, connection, encryption, latency, server work, serialization, parsing, battery, user time. Don't return fields the component doesn't read.

// BAD: unnecessary fields (images, supplier, reviews, metadata)
// GOOD: only what the component uses
{"id": 1, "name": "linen shirt", "price": 2300}
Enter fullscreen mode Exit fullscreen mode

Rule 3 — images: real resized sets, not one giant file

Shipping a 2400×1800 photo for a 300×225 thumbnail transports data you never need. Generate a set — 160/400/800/1600 px — and let the browser pick:

<img
  srcset="img-160.webp 160w, img-400.webp 400w, img-800.webp 800w"
  sizes="(max-width: 600px) 100vw, 400px"
  src="img-400.webp"
  alt="product">
Enter fullscreen mode Exit fullscreen mode

Note: lazy loading ≠ size optimization. loading="lazy" delays when a file downloads; it doesn't make it smaller.

Rule 4 — run less code

  • Debounce a search box so c, ca, cam, cama fire one request, not four.
  • Memoize f(x) when recomputation is cheap compared to the network.
  • Virtualize long lists: 10,000 rows → render the ~20 visible.

Rule 5 — cache explicitly, with boundaries

Pattern: cache hit → return; miss → API → store → return. Benefits: latency, requests, server load, and — decisively — less dependency on the network.

Boundaries: memory is finite, so pick an eviction policy. LRU (Least Recently Used) evicts the least-recently-touched entry. I wrote adev-lru for exactly this. The real decision isn't the library — it's which information deserves to stay available locally.

Rule 6 — a cache can lie; bound the truth

DB says $550, cache says $500 → two versions of reality. Controls: TTL, invalidation, stale-while-revalidate, ETag, versioning, cache-aside.

Offline-first ≠ offline forever. Show cache → consult server → update.

Rule 7 — writing with no network: a local queue

Order created, address edited, product removed, all offline → hold operations in a local queue, sync when the network returns.

The question flips from "how do I make the internet never fail?" to "how does my app survive when it fails?"

Rule 8 — idempotency (the duplicate order)

Server got the POST; the 200 OK was lost; client retries → orders #123 and #124.

POST /orders
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Enter fullscreen mode Exit fullscreen mode

Retry with the same key → same order, no duplicate. A network failure is not an operation failure.

Rule 9 — conflicts between devices (a lesson in smaller than you think)

Two offline devices edit the same record: price 500 vs 550. You now own a distributed-system problem. Options, with trade-offs:

  • optimistic concurrency / versioning — reject on write conflict, require the user to reload;
  • timestamps + last-write-wins — simple, silently loses the older edit;
  • eventual consistency — eventually everybody agrees; decide if the data tolerates it.

Pick explicitly; a cache is a place where "the data tolerates it" should be questioned.

Where this belongs

Performance as a later stage (build → publish → measure → optimize) gets you a fast app on a good network and nothing on a bad one. Under hard constraints the decisions live in the architecture: how much is downloaded and stored, and what happens when the connection fails.

TL;DR

  1. Never send what isn't needed.
  2. An API response is a payload, not a promise — return used fields only.
  3. Images: resized sets + srcset/sizes + modern formats; lazy loading doesn't shrink files.
  4. Debounce, memoize, virtualize — run less code.
  5. Cache with a boundary (LRU) and correct-yourself mechanisms (TTL/SWR/ETag).
  6. Offline writes belong in a queue; retries need an Idempotency-Key.
  7. Conflicts are distributed-system problems, even on one laptop.

Internet is a dependency. Architecture decides how much of it you need.


Adapted from my original post on TallerWeb.

Top comments (0)