A mobile screen makes a dozen API calls and loads slowly on cellular
“The home screen is instant on office Wi-Fi, but on a train it shows spinners for several seconds. The network log shows a dozen small requests, several of which only start after another one finishes.”
THE PRINCIPLE
Latency is paid per serial round trip. Count the steps on the critical path, not the total requests.
FIRST MOVES
- Measure on a real device under a throttled or real cellular profile (Android Studio Network Inspector, Xcode Instruments' HTTP Traffic, or a proxy) and draw the waterfall: which requests wait on others, and which repeat.
- Break component-level fetch-on-mount chains: start the screen's data at navigation intent (tap, app start) and read it from a shared cache, so child components don't each trigger their own request after render.
- Replace the dozen calls with one screen-shaped endpoint: a backend-for-frontend or composite endpoint that fans out in parallel next to the services, or a single GraphQL query. Keep it bounded (limits, pagination) so one call doesn't become one huge payload.
- Reuse connections and use HTTP/2 or HTTP/3 so parallel requests share one connection; this removes connection setup, not dependency waits between requests.
- Cache and defer: show cached data first, revalidate in the background, and load below-the-fold or optional sections after the first paint.
TOOLS: THEN → NOW
One generic REST resource per entity, composed on the deviceA BFF or composite endpoint per screen, or GraphQL with persisted queries
Fetch in each component's mount effectRoute- or intent-level prefetching into a shared query cache
Testing on simulator over fast Wi-FiProfiling on device with network throttling and real-network traces
PATTERN SNAPSHOT
// Before: Header, Feed and Badges fetch on mount, and Feed waits for /me: 3+ serial trips.
// After: one screen-shaped call, started on tap, before the screen mounts.
let home: Promise<HomeScreen> | undefined;
export function loadHome(): Promise<HomeScreen> {
home ??= fetch(`${API}/bff/home?feedLimit=20`).then((r) => {
if (!r.ok) throw new Error(`home ${r.status}`);
return r.json() as Promise<HomeScreen>;
});
home.catch(() => { home = undefined; }); // don't cache failures
return home;
}
// onPress: loadHome(); navigate("Home"); The screen awaits the same promise.
// In a real app your query cache (TanStack Query, SWR, Apollo) plays this role.CLOSE THE AI. EXPLAIN THIS.
A screen makes twelve requests, but only three of them are serial. On a 150 ms round-trip network, roughly what is the minimum network time, and which change helps more: HTTP/3 or removing one serial step?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- Azure Architecture Center: Chatty I/O antipattern (opens in new tab)Checked
- Azure Architecture Center: Backends for Frontends pattern (opens in new tab)Checked
- Android Developers: Inspect network traffic with the Network Inspector (opens in new tab)Checked
- Apple Developer: Analyzing HTTP traffic with Instruments (opens in new tab)Checked
Guide reviewed