We switched from eager-loading everything at launch to lazy-loading with a manifest-based dependency graph. The wins came from:
Mithril for navigation instead of React Native. Smaller bundle, faster first paint. Navigation state is just data. We went from 4.2MB to 2.1MB unpacked.
Hermes engine because v8 startup is garbage for cold starts. First execution is 60% faster. Yeah it has quirks with certain polyfills but worth it.
Precompiled bytecode instead of parsing JS on every launch. We use metro's experimental bytecode output. Cuts parse time from ~800ms to ~120ms.
SQLite for local cache not AsyncStorage. Queries are way faster when you have 10k+ items. We cache the app manifest there too so we only fetch what changed.
const manifest = await db.query('SELECT * FROM manifest WHERE version = ?', [VERSION]);
if (manifest) {
skipRemoteCheck = true;
} else {
// fetch and hydrate
}
The hard part wasn't the tools, it was profiling correctly. We were guessing until we actually looked at flame graphs. flamegraph.js was clutch.
Still have jank on first frame of complex screens but acceptable tradeoff for now.
Tom Lindgren
Senior dev. PostgreSQL and data engineering.
Nice work on the startup time. A couple observations from running similar optimization work.
Hermes is solid for cold starts but watch your production metrics closely. We saw weird memory spikes under load that weren't obvious in testing. The bytecode precompilation helps but you're trading parse time for cache invalidation headaches on updates.
The real win here is probably the manifest-based dependency graph. That's the hard part. Most teams never get there because it requires discipline across the team.
One thing: make sure you're measuring what actually matters. App cold start is great but what's your p95 first interaction time? We optimized ourselves into a corner once chasing startup metrics while users waited on network calls that happened immediately after.