Performance budgets
Status: Reference
Round 30 production-readiness — documented budgets + how to verify them.
Mobile (Hermes bundle)
| Platform | Budget | Current (commit bd81763 per OTA logs) | Headroom |
|---|---|---|---|
| iOS | 8 MB | 7.6 MB | +5% |
| Android | 9 MB | 7.7 MB | +17% |
How to measure: yarn size from repo root. Runs expo export for both platforms with --source-maps external, then walks apps/mobile/dist/_expo/static/js/{ios,android}/ for the entry Hermes bundle and compares against the budget defined in scripts/measure-bundle-size.mjs. Measure the deployable .hbc bytecode with source maps split out; source maps must be uploaded/retained as separate release artifacts, not counted as runtime startup code.
Why not in CI? expo export is ~3–5 min × 2 platforms. Adding it to every PR would dominate CI time. Run manually after features that touch many files; if the bundle grows >2% in a single commit, investigate.
Bumping the budget: do it explicitly in measure-bundle-size.mjs with a comment naming the feature that justified the bump. Don't silently ratchet upward.
What contributes: every package in @twomore/app and @twomore/ui plus its transitive deps. Biggest contributors today: React Native, Tamagui, TanStack Query, Reanimated, Sentry. New native modules (e.g., Kakao SDK Phase 8) trigger a runtime version bump AND likely a budget bump.
Web (Core Web Vitals)
Every page reports vitals to /api/vitals via WebVitalsReporter. The endpoint logs to client_errors with severity='info' and context.vitals=true — same dashboards/queries that surface error rates slice by perf percentile.
Targets (Google's recommended thresholds):
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5 s | 2.5–4.0 s | > 4.0 s |
| INP (Interaction to Next Paint) | ≤ 200 ms | 200–500 ms | > 500 ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1–0.25 | > 0.25 |
| FCP (First Contentful Paint) | ≤ 1.8 s | 1.8–3.0 s | > 3.0 s |
| TTFB (Time to First Byte) | ≤ 0.8 s | 0.8–1.8 s | > 1.8 s |
With experimental.webVitalsAttribution: ['CLS', 'LCP', 'INP'] enabled in apps/web/next.config.ts, every captured metric includes attribution data identifying WHICH element / interaction / layout shift triggered the score. Query examples:
-- LCP P75 over last 7 days
SELECT percentile_cont(0.75) WITHIN GROUP (ORDER BY (context->>'value')::float)
FROM client_errors
WHERE context->>'vitals' = 'true'
AND context->>'name' = 'LCP'
AND created_at > now() - INTERVAL '7 days';
-- INP "poor" by route
SELECT route, COUNT(*)
FROM client_errors
WHERE context->>'vitals' = 'true'
AND context->>'name' = 'INP'
AND context->>'rating' = 'poor'
GROUP BY route
ORDER BY COUNT(*) DESC;Action thresholds: if INP P75 > 200ms for any route, profile via Chrome DevTools (Performance > Web Vitals panel) and identify the slow handler. Most common causes: synchronous JSON parsing, non-deferred state updates inside onPress, large lists rendering without virtualization.
Mobile (FPS + JS thread)
Sentry profiling enabled at 10% prod sample rate (profilesSampleRate in packages/app/src/lib/sentry.ts:34). Flamegraphs appear in Sentry Performance tab → Profiles. No budget enforced; profiling is a diagnostic tool, not a gate. If specific screens show > 16ms frames (60fps target), use the existing usePrefersReducedMotion pattern + applyOptimisticPatches to keep work off the UI thread.
Bundle observability per OTA
Every yarn ota:preview prints bundle sizes:
[expo-cli] _expo/static/js/ios/entry-<hash>.hbc (7.6MB)
[expo-cli] _expo/static/js/android/entry-<hash>.hbc (7.7MB)These get captured in commit messages by convention (see Round 24's OTA group output in CLAUDE.md). When the number moves >2% from the prior OTA, note it.
Future automation (not yet shipped)
- CI gate for mobile bundle size: requires either (a) caching
expo exportoutput across runs or (b) running it nightly instead of per-PR. Defer until bundle growth becomes a real problem. - Source-map analyzer:
metro-visualizerorreact-native-bundle-visualizerfor chunk-level breakdowns. Useful when a specific dep needs to be slimmed. - Web Lighthouse CI: full PageSpeed run as part of release. Skip for now; the Web Vitals stream gives equivalent signal continuously rather than per-deploy.