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}
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">
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,camafire 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
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
- Never send what isn't needed.
- An API response is a payload, not a promise — return used fields only.
- Images: resized sets +
srcset/sizes+ modern formats; lazy loading doesn't shrink files. - Debounce, memoize, virtualize — run less code.
- Cache with a boundary (LRU) and correct-yourself mechanisms (TTL/SWR/ETag).
- Offline writes belong in a queue; retries need an
Idempotency-Key. - 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)