The situation
Say you are a mobile engineer on a small React‑Native team that uses Expo Application Services (EAS) for CI‑driven builds and over‑the‑air (OTA) updates. The product manager has just asked the team to “make the app faster”. Within a week the QA team reports that OTA download times have risen from ~3 s to >7 s on a 3G connection, and the build queue on the CI server now averages 45 min per commit instead of 30 min. The delay costs the team two sprint cycles: developers wait for builds, users abandon the update flow, and the next release risks missing a hard deadline.
The symptom is clear—build artefacts are growing, and the OTA payload size is no longer acceptable for low‑bandwidth users. The vague request “make it faster” cannot be acted on until the team agrees on a measurable ceiling that can be defended in code review and CI.
Why it happens
-
No single source of truth for size or time – Expo’s default
eas buildcommand prints the final IPA/APK size, but that value lives only in the build logs. Without a persisted metric, each developer estimates the impact of a new library or asset by eye. - OTA updates are incremental but the baseline shifts – When a new native module is added, the bundle hash changes and the next OTA diff may include the entire bundle, not just the delta, because the diff algorithm falls back to a full upload if the manifest version changes.
-
CI pipelines treat build time as a side‑effect – The default EAS workflow runs
eas build --profile productionand then uploads the artefact. No step fails the job if the artefact exceeds a size threshold, so the pipeline happily produces a 150 MB IPA even though the target budget is 100 MB. - Performance budgets are often treated as “nice‑to‑have” – Without a hard gate, developers may accept a temporary overshoot, assuming it will be fixed later. That habit erodes the budget over time and makes the eventual “fix” larger and riskier.
These root causes mean the team cannot reliably translate “make it faster” into a concrete, enforceable rule.
The fix, step by step
| Step | Action | Why | How to verify |
|---|---|---|---|
| 1 |
Define the budget – pick a single metric that directly reflects the user‑visible problem. For OTA latency on 3G, the dominant factor is the compressed bundle size (the *.js bundle after Metro’s --minify and --optimize flags). For build‑time pressure, use total CI wall‑clock time from eas build start to artifact upload. |
A single, measurable target turns “faster” into a number the CI can enforce. | Write the budget in the repo’s README.md, e.g. “OTA payload ≤ 12 MB gzipped, CI build ≤ 35 min”. |
| 2 |
Instrument the metric – add a script that runs after the Metro bundler and outputs the exact size. Create scripts/measure‑bundle.js: |
js\nconst fs = require('fs');\nconst zlib = require('zlib');\nconst bundlePath = process.argv[2];\nconst raw = fs.statSync(bundlePath).size;\nconst gzipped = zlib.gzipSync(fs.readFileSync(bundlePath)).length;\nconsole.log(`RAW=${raw} GZIP=${gzipped}`);\n
Run it in the eas build pre‑publish hook. | Raw size alone is misleading because OTA uses gzip on the server; we need the actual transmitted size. | After a build, the CI log should contain a line like RAW=13 524 321 GZIP=6 842 109. |
| 3 | Add a CI gate – modify the EAS workflow file (eas.json) to invoke the script and fail if the size exceeds the budget. Example fragment:
json\n\"build\": {\n \"production\": {\n \"prePublish\": [\"node scripts/measure-bundle.js ./dist/main.js\"],\n \"postPublish\": [\"node scripts/enforce-budget.js\"]\n }\n}\n
enforce-budget.js reads the environment variable GZIP and exits with code 1 if GZIP > 12_000_000. | CI now blocks any commit that pushes the OTA payload over the limit, preventing regression. | Run eas build --profile production locally; if the bundle is 13 MB gzipped, the build should abort with Error: OTA payload exceeds 12 MB budget. |
| 4 | Track build time – wrap the eas build command in a timer script. scripts/timed‑build.sh:
bash\n#!/usr/bin/env bash\nSTART=$(date +%s)\neas build \"$@\"\nEND=$(date +%s)\nELAPSED=$((END-START))\necho \"BUILD_TIME=${ELAPSED}s\"\nif [ $ELAPSED -gt 2100 ]; then # 35 min = 2100 s\n echo \"Error: Build time exceeds 35 min budget\"\n exit 1\nfi\n
| Build‑time spikes (e.g., after adding a heavy native module) cause the CI to fail early, prompting optimisation before merge. | In CI logs you should see BUILD_TIME=1874s. If the time is 2500 s, the job aborts with the error message. |
| 5 | Automate diff‑size checks for OTA – Expo’s OTA service stores a manifest with the previous bundle hash. Add a step that runs expo export --output-dir ./dist and then node scripts/compare‑bundles.js ./dist/previous ./dist/current. The script calculates the delta size and fails if the delta > 5 MB (a reasonable incremental budget). | Prevents a full‑bundle upload caused by an accidental manifest bump, which would spike OTA latency for all users. | After a successful incremental change, the CI log shows DELTA=1 324 567. If a change pushes it to 6 MB, the script exits 1 and the build fails. |
| 6 | Document the trade‑offs – add a docs/performance‑budget.md that explains why the 12 MB OTA ceiling was chosen (3G median bandwidth ≈ 1.5 Mbps, 8 s target). Include a table of common actions and their impact (e.g., “Add react-native-reanimated → +0.8 MB gzipped, +2 min build”). | When a developer asks “why is this failing?”, the doc provides the rationale, reducing debate. | Open the markdown; the first line should read “Budget: OTA payload ≤ 12 MB gzipped (≈ 8 s on 3G)”. |
| 7 | Monitor in production – enable Expo’s expo-updates metrics (EXPO_UPDATES_CHECK_AUTOMATIC=1) and ship a small telemetry snippet that reports downloadDurationMs. Store the median in a dashboard (e.g., Datadog). Set an alert if median > 9 000 ms. | Guarantees the budget remains realistic; if network conditions improve, the budget can be relaxed safely. | In the dashboard you should see a time‑series; a spike above the threshold triggers an alert. |
| 8 | Iterate – after each release, review the CI logs and production metrics. If the OTA size is consistently well under 12 MB, consider raising the budget to accommodate a new feature; if it regularly hits the ceiling, audit the bundle for dead code or large assets. | Keeps the budget aligned with product velocity and user experience expectations. | Compare the latest GZIP value with the budget; adjust the number in README.md and enforce-budget.js as needed. |
Following these steps gives the team a hard, testable ceiling that can be defended in code review, communicated to product, and enforced automatically.
What it costs, and when not to do it
| Cost | Description | When the cost outweighs the benefit |
|---|---|---|
| Initial engineering time – ~2 days to write the measurement scripts, CI hooks, and documentation. | Small teams with a single release per quarter may find the upfront effort disproportionate. | If the app is internal‑only and OTA updates are infrequent, a budget may be unnecessary. |
Longer CI runs – the timed‑build.sh wrapper adds negligible overhead, but the expo export and diff‑size comparison add ~30 s to the pipeline. |
Projects with extremely tight CI windows (e.g., < 5 min total) may see the extra steps cause queue bottlenecks. | When the CI is already saturated and adding a 30 s step would push builds past the acceptable window, consider moving the diff check to a nightly job instead of every PR. |
| Potential for over‑engineering – enforcing a strict OTA size may lead to premature code‑splitting or lazy loading that harms maintainability. | If the app’s user base is predominantly on 4G/5G, the 12 MB ceiling may be unnecessarily tight, causing unnecessary refactoring. | In a high‑bandwidth market, focus on other performance aspects (e.g., UI thread jank) rather than OTA size. |
Operational complexity – maintaining the docs/performance‑budget.md and keeping the budget numbers up to date requires discipline. |
Teams with high turnover may lose the knowledge of why the budget exists, leading to accidental regressions. | If the team cannot guarantee documentation upkeep, adopt a simpler “monitor‑only” approach: alert on regressions without failing the CI. |
When not to adopt:
- Apps that never use OTA (e.g., native‑only builds).
- Projects where the bundle is already tiny (< 5 MB gzipped) and the OTA latency is negligible.
- Teams that lack a CI system capable of custom scripting (e.g., only using Expo’s managed build dashboard).
In those cases, a lightweight monitoring alert may be sufficient.
At your level
| Role | Take‑away | Immediate next step |
|---|---|---|
| Student / first‑year engineer | A performance budget is just a numeric threshold you can enforce with a simple script. | Add a measure‑bundle.js script to a sample Expo project and run it locally to see the gzipped size. |
| Working engineer (feature shipper) | Embed the budget check in your PR pipeline so a failing build stops the merge before it reaches production. | Fork the CI script from this article, set a provisional budget (e.g., 15 MB), and add it to your repo’s CI config. |
| Senior / staff engineer | Use the budget as a negotiation point with product: the number (e.g., 12 MB) is a concrete cost of a new dependency. | Review the current OTA payload, compute the delta for a planned library, and present the impact in a short Slack message. |
| Lead or director | The budget protects the release schedule and user experience; its cost is a modest increase in CI time and documentation effort. | Allocate a one‑week sprint slot for “performance‑budget implementation” and assign an engineer to own the documentation. |
In the interview
Typical interview prompt
“Our mobile app is built with Expo and uses OTA updates. We’ve been told to ‘make it faster’, but we don’t have any concrete target. How would you turn that vague request into something the team can enforce?”
Weak answer
- “I would just look at the bundle size and try to reduce it by removing unused libraries.”
- Why it’s weak: It never defines a measurable target, does not explain how to enforce it, and ignores build‑time or OTA‑specific constraints.
Strong answer
- Trade‑off: “We need a budget that balances OTA latency on 3G against developer productivity. The dominant factor is gzipped bundle size, because OTA serves the compressed payload.”
- Constraint: “Our telemetry shows a median OTA download time of 8 s on 3G, which corresponds to roughly 12 MB gzipped.”
- Failure mode: “If a new native module bumps the manifest version, the diff algorithm will fall back to a full upload, instantly breaking the latency target.”
-
Number: “Set the OTA payload ceiling at 12 MB gzipped and the CI build‑time ceiling at 35 min. Enforce both with a pre‑publish script that fails the build if
GZIP > 12_000_000orBUILD_TIME > 2100seconds.” -
Implementation: “Add a
measure‑bundle.jsscript, wrapeas buildin a timer, and add a CI gate ineas.json. Document the rationale indocs/performance‑budget.mdand monitor real‑world download times viaexpo-updatesmetrics.”
How the answer scales with seniority
- Junior: Expected to describe the basic steps (measure size, add a CI check).
- Senior / staff: Should discuss the rationale for the chosen metric, the impact on OTA diff behaviour, and how to keep the budget realistic over time (monitoring, alerting, iteration).
- Director / engineering manager: Should frame the budget as a risk‑mitigation tool, quantify the cost of missed deadlines, and propose a process (e.g., a quarterly budget review) to align product scope with performance constraints.
Follow‑up that separates experience from theory
“Can you walk me through a real incident where a change to the manifest caused a full OTA upload, and how you detected and resolved it?”
A candidate who has actually dealt with the diff‑fallback will describe the manifest bump, the sudden spike in OTA payload size, the CI failure, and the fix (e.g., keeping the manifest version unchanged and only updating the bundle hash). That demonstrates hands‑on experience beyond reading documentation.
Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: www.amitchakraborty.dev · LinkedIn · GitHub. Open to senior and founding engineering roles, remote worldwide.
Top comments (0)