shipping/JOG — 03:41 GMT+7/48.3 °C CPU/v 07.0 · build 2026.05.12
lat −7.7956 · lon 110.3695/ntwk · online/open for projects
back to notesnotes2026react-native-indonesia-performance
[ essay no. 10 ]engineeringmay 20, 20265 min995 wordsrevision 1live

React Native on Indonesian 3G

Shipping a learning app to 500+ UGM students taught me what the React Native docs don't say about cheap Android phones on a tethered 4G/3G network. The budget I actually defend.

d
devrangga hazza mahiswaracreative engineer · jogja, id
share on 𝕏

React Native on Indonesian 3G

500+students reached
1code tree, two platforms
Feb → May 20244 months · solo
<2.5sfirst-paint budget I defend

The Edus.id launch month taught me that the React Native docs are calibrated for a different physics. The official perf advice assumes recent iPhones on US WiFi. My audience was UGM students on Android phones two-to-four years old, connected to whatever university network or tethered phone hotspot they had, sometimes routed through 3G fallback when the campus 4G saturated mid-day.

The performance budget for that audience isn't what the React Native docs recommend. It's what works on a Redmi Note 9 with 2 bars of signal in a crowded lecture hall. This post is the budget I actually defend now, and how I learned the numbers.

01

the constraint nobody documents

Edus.id was a learning surface for UGM students. The brief was interactive learning, native feel, available across cohort devices. The constraint that ended up driving every decision was the long tail of student devices.

The 80th-percentile device wasn't recent. It was a 2-3-year-old mid-range Android. Often 3GB RAM. Often a network connection that varied between strong 4G and degraded 3G across the course of a single class.

The mistake I made early was designing for the median device on the median network. The median device worked fine. The 80th percentile crashed.

02

the budget i defend

The numbers I now hold the app to:

  • JS bundle (initial download): under 1 MB compressed, ideally 700 KB
  • Time to interactive (cold start, on the 80th-percentile device): under 2.5 seconds
  • Frame budget for any interaction: 16ms; never miss two frames in a row on a list scroll
  • List render ceiling: never load more than 50 items into the JS thread at once
  • Image strategy: always use Image with a placeholder, never let layout shift on first load, AVIF or WebP, never PNG

The numbers are tight. They have to be. Every 200ms of TTI costs about 5-8% of the cohort in terms of "users who give up before the app finishes loading."

03

the graphql contract was the right call

The single technical decision that paid off most was using GraphQL for the API contract.

Why: TypeScript types were generated from the schema, so iOS and Android shared the same wire format. When I changed a field on the backend, the schema regenerated, both clients got the new type, and any code that depended on the old shape failed at type-check before runtime. The number of cross-platform field-mismatch bugs I'd have shipped without this is uncomfortable to estimate.

Cost: GraphQL has a learning curve and the React Native client tooling around it (Apollo, urql, etc.) adds bundle weight. The trade-off was worth it for the contract guarantee. For a smaller surface I might pick a typed REST client instead.

04

the three patterns that ate the most time

List virtualization for slow devices. The default FlatList works for 50-100 items on a decent device. At 200+ items on a mid-range Android, it starts dropping frames on scroll. I moved to FlashList (Shopify's faster implementation) and got back another ~150% of headroom. If list performance is unstable, this is the single biggest win.

Image loading and placeholder strategy. Out-of-the-box Image doesn't show anything until the bitmap is decoded. On slow networks that's a blank rectangle for 1-3 seconds. The fix: always paired with a blurhash or LQIP placeholder, and accept the small bundle cost of decoding the placeholder format. The user sees something immediately; the real image fades in.

Network-conscious data fetching. I wrote a small wrapper around the GraphQL client that exposes the connection type (@react-native-community/netinfo) to queries. On 3G, I limit per-query data to ~50 KB and lazy-load secondary fields on demand. On 4G+, I pre-fetch more aggressively. Same code path; different defaults based on detected network.

05

what i'd do differently

If I started Edus.id today:

  1. Ship offline support from day one. The single biggest complaint from the cohort was the app doesn't work when I'm on the train home. Adding offline sync after launch was 3x the work it would have been to design it in. The architectural retrofit was painful.
  2. Use React Native's Hermes engine from project start. I migrated mid-project; the engine swap improved cold start by ~22% but cost me a week of fixing edge cases. If I'd started on Hermes, no migration tax.
  3. Test on the 80th-percentile device throughout the build, not at the end. I shipped two weeks of perf optimizations in the final two weeks. Most of them could have been avoided by testing on a Redmi Note 9 from week 1.
06

the meta-lesson

The performance work that matters most isn't the work that produces dramatic before/after numbers. It's the work that moves the floor — the slowest device's experience, the worst network's behavior, the user who would have given up.

Most performance content is written by engineers shipping for audiences indistinguishable from themselves. The advice rarely accounts for the long tail. If your audience includes anyone on a non-recent Android, a non-broadband network, or a non-Western infrastructure path, the budget is tighter than the docs suggest.

The Edus.id launch was the first project where I had to defend a real performance budget against real users on real devices. The instinct it built — who is my 80th-percentile user; what does the app feel like for them? — is the single most valuable thing I took into every project since.

— end of essay · published may 20, 2026 · 995 words · 5 min
[ if this moved you ]

keep reading.

three essays in the same key · pick one