March 16, 2026·10 min read
MMKV vs AsyncStorage: pick with your eyes open
AsyncStorage is simple and everywhere. MMKV is fast and synchronous-feeling. Both are wrong if you store huge JSON blobs or treat them like a real database. Choose based on read/write patterns and team comfort with native dependencies—not benchmarks alone.
What you’re optimizing
If you read settings keys a few times per session, AsyncStorage is probably enough. If you thrash storage on every keystroke, MMKV’s speed shows up in traces.
Measure before swapping; native modules aren’t free either.
Migration path
Read old keys, write new keys, ship, then delete legacy after a version or two. Feature-flag the migration if the audience is large.
Don’t block UI on migration; show a splash or background it.
Security posture
Neither replaces encrypted storage for tokens if your threat model says so. Use the right tool: SecureStore/expo-secure-store for secrets, key-value for preferences.
Document max size expectations so someone doesn’t cache megabytes of images “for offline.”
A practical decision shortcut
Stick with AsyncStorage if your storage access is rare and payloads are small. Consider MMKV when profiling shows storage on a hot path (feature flags, draft text, high-frequency toggles) or when you need more predictable read latency on low-end Android.
Never store PII or auth tokens in either without a deliberate threat-model conversation—speed does not fix compliance.
Workload profiles that matter
Measure read/write frequency and payload sizes with realistic usage, not synthetic loops. Settings toggles, cached feature flags, and draft text behave differently from media metadata caches. MMKV’s synchronous API can simplify code paths but does not excuse doing heavy JSON parse/stringify on every frame. AsyncStorage is fine for infrequent, small blobs. If you batch writes, consider debouncing to reduce flash wear on older Android devices—rare but real at scale.
Migration strategies without downtime
Read legacy keys on startup, write new schema, feature-flag rollout, and retire old keys after adoption thresholds. Avoid blocking UI during migration; show progress if unavoidable. Log migration failures with anonymized device stats. For large user bases, stage migrations across versions to reduce support load. Backup critical user data server-side when business rules allow—local storage is not durable backup.
Security boundaries
Neither MMKV nor AsyncStorage replaces hardware-backed secure storage for high-value secrets under strong threat models. Use platform secure storage for tokens when appropriate. Encrypt sensitive local caches if policy requires—know your key management story. Be mindful of backups: iOS backups may include files unless excluded—consult platform docs for sensitive data.
Debugging storage issues
Corruption, partial writes, and version skew between app binaries cause subtle bugs. Add checksums or schema versions for critical blobs. Provide internal tools to dump storage state in dev builds—never in production without gating. When users report ‘logged out randomly,’ trace storage clears, OS low-storage events, and app updates that migrate data.
Multi-process and extension caveats
If you add app extensions or share data with widgets, storage location and locking matter. MMKV supports multi-process modes but requires correct configuration. Race conditions between JS and native can corrupt data—serialize access. Test background fetch and killed-state relaunch paths that read storage early.
Cost-benefit summary
Choose MMKV when profiling shows storage latency on critical paths or when synchronous reads simplify correctness. Stay on AsyncStorage when payloads are small and rare—less native surface area. Re-measure after RN upgrades; storage performance characteristics shift with Hermes and bridge changes.
Checklist before switching
Profiled hot paths, migration plan documented, secure storage story unchanged, QA on low-end devices, crash analytics in place, rollback plan if metrics regress. Storage swaps are easy to undertest—treat them like mini releases.
Shipping and reliability habits (1)
Testing onboarding changes with funnel metrics beats debating opinions. Segment by acquisition channel and platform; back behavior differs. Skip paths must be genuine—dark patterns may win short metrics and destroy brand trust. Localization length tests prevent clipped CTAs in verbose languages.
Background execution policies change with OS updates—revalidate after major iOS and Android releases. Misused background modes invite rejection. Persist user work frequently; the OS can kill you anytime after backgrounding. Uploads and timers should tolerate pause and resume without corrupting state.
Platform differences worth rehearsing (2)
Shipping React Native features is less about any single API and more about the system around it: typed boundaries, predictable navigation, and telemetry that tells you what broke in production. Prefer boring, explicit modules over clever metaprogramming that the next hire cannot grep. When platform vendors change behavior in point releases, your defense is automated smoke tests on real devices and a short internal changelog of native assumptions you rely on.
Performance work should start with measurement, not instinct. Watch JS thread versus UI thread separately; they bottleneck differently. Lists, images, and animations dominate most regressions—optimize those before micro-optimizing pure functions. Hermes, JSC, and bridge internals evolve; re-profile after every major upgrade instead of trusting last year’s numbers. Battery and thermal throttling on mid devices reveal issues flagship phones hide.
Security, privacy, and data handling (3)
Push notifications walk a line between helpful and intrusive. Prime users with context, respect notification channels on Android, and measure opt-outs after campaigns—spikes mean copy or frequency problems. Payload design affects background behavior; test killed and locked-device states. Tokens belong server-side with rotation strategies; never treat the client as authoritative for subscription state.
Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.
Performance and measurement discipline (4)
Type-safe navigation pays off when routes multiply. Keep param lists near navigators, validate external URLs, and avoid serializing non-JSON-safe values through params. Renaming routes is a cross-cutting change—update analytics, push payloads, and E2E selectors in the same release train.
FlatList performance is configuration as much as code. Stable keys, reasonable `windowSize`, and memoized rows beat switching to a different list primitive blindly. Nested virtualized lists are a last resort—redesign first. Profile with production-like data volumes; dev placeholders lie.
Team process and long-term maintenance (5)
Reanimated and gesture libraries earn their place when profiling proves UI-thread work and your team can maintain native upgrades. Worklets have constraints—read errors carefully. Respect reduced motion and test Android timing differences—identical JS does not guarantee identical feel.
Analytics schema governance prevents warehouse disasters: version events, avoid high-cardinality strings, and align names across iOS, Android, and web. Consent gating must stop network calls, not just UI. Separate dev and prod projects to avoid polluting dashboards.
Shipping and reliability habits (6)
Project structure should make ownership obvious: routes as backbone, feature folders for product areas, thin screens, and shared infrastructure that is deliberately named. Refactor in vertical slices with device-tested releases—big-bang rewrites without tests are how teams lose weeks.
Helper modules concentrate glue code—storage, navigation, permissions—so screens stay readable. Split helpers by topic before files become merge-conflict magnets, and document each module’s contract. Good helpers answer ‘where do we save tokens?’ in one glance—not ‘ask Sarah.’
Platform differences worth rehearsing (7)
Native modules are product decisions disguised as engineering tasks. You inherit Xcode and Gradle upgrades, store review scrutiny, and security obligations. Prefer maintained Expo modules and config plugins before writing JNI or Swift glue from scratch. When you must go native, budget pairing time with platform specialists and write runbooks for on-call—crashes in native code bypass many JS safeguards.
Deep links are a cross-team system: marketing URLs, hosted association files, entitlements, router params, and analytics query preservation. Debug with structured logging of raw URLs (scrub secrets) and reproduce cold-start races with auth hydration. Staging and production should be obviously separated—accidentally opening prod from a QA link erodes trust and pollutes data.
Security, privacy, and data handling (8)
OTA updates are powerful and risky: runtime compatibility, rollback plans, and user-visible behavior changes need governance. Channels should map to release maturity—staging versus production—with access controls on publish credentials. Large assets over cellular need care; silent failures erode trust more than a frank ‘update failed, retry’ message.
Keyboard and form UX separate polished apps from ‘works on desktop simulators.’ Platform differences in soft input modes matter; test smallest phones and Android gesture navigation. Primary actions must remain reachable when the keyboard is visible—scroll containers and keyboard controllers exist because this problem is universal.
Performance and measurement discipline (9)
Expo SDK upgrades are integration projects: `expo doctor`, aligned community packages, regenerated native projects, and device smoke tests for camera, push, and IAP. Freeze unrelated native refactors during the upgrade window and keep rollback paths hot. Document surprises for the next upgrade while memory is fresh.
Hermes versus JSC is not a lifestyle choice—profile your app. Hermes usually wins on startup; some libraries still assume JSC quirks. Engine toggles are not substitutes for fixing quadratic renders in your own code. Upgrade notes matter: Intl support and debugging tooling evolve.
Team process and long-term maintenance (10)
Metro cache issues masquerade as logic bugs. Establish a documented reset ladder: dev server restart, cache flags, Watchman, derived data, then dependency reinstalls. Compare platforms when only one breaks—native steps diverge. Keep CI caches deterministic with lockfiles and pinned toolchains.
Environment variables should be classified: public-by-design, sensitive-with-mitigations, or never-on-device. `EXPO_PUBLIC_` values are extractable—treat them that way. Align env handling across EAS profiles and local dev; fail fast when keys are missing instead of shipping undefined behavior.
Shipping and reliability habits (11)
Analytics schema governance prevents warehouse disasters: version events, avoid high-cardinality strings, and align names across iOS, Android, and web. Consent gating must stop network calls, not just UI. Separate dev and prod projects to avoid polluting dashboards.
Testing onboarding changes with funnel metrics beats debating opinions. Segment by acquisition channel and platform; back behavior differs. Skip paths must be genuine—dark patterns may win short metrics and destroy brand trust. Localization length tests prevent clipped CTAs in verbose languages.
Platform differences worth rehearsing (12)
Environment variables should be classified: public-by-design, sensitive-with-mitigations, or never-on-device. `EXPO_PUBLIC_` values are extractable—treat them that way. Align env handling across EAS profiles and local dev; fail fast when keys are missing instead of shipping undefined behavior.
Project structure should make ownership obvious: routes as backbone, feature folders for product areas, thin screens, and shared infrastructure that is deliberately named. Refactor in vertical slices with device-tested releases—big-bang rewrites without tests are how teams lose weeks.
Security, privacy, and data handling (13)
Accessibility is compatibility. Labels, focus order, and dynamic type are not polish—they determine whether users can complete tasks at all. Test with VoiceOver and TalkBack on hardware; simulators miss focus bugs. When designs prioritize minimalism, negotiate text alternatives for icon-only controls. Accessibility regressions often follow navigation redesigns—add checklist items to those PRs specifically.
Internationalization is a product feature, not a string swap. Plural rules, RTL layout, and locale-aware formatting change behavior—not just copy length. Pseudolocale helps find clipping early, but real Arabic and German QA catches nuance. Avoid concatenating translated fragments; context matters. Document glossary terms so translators do not invent inconsistent product names.
Performance and measurement discipline (14)
Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.
Error boundaries catch render failures, not native crashes or async mistakes. Pair them with platform crash reporting and structured client logs. Fallback UI should include build identifiers and humane copy—never raw stack traces for end users. Test fallbacks with screen readers; a broken error screen is still broken UX.
Team process and long-term maintenance (15)
Keyboard and form UX separate polished apps from ‘works on desktop simulators.’ Platform differences in soft input modes matter; test smallest phones and Android gesture navigation. Primary actions must remain reachable when the keyboard is visible—scroll containers and keyboard controllers exist because this problem is universal.
Type-safe navigation pays off when routes multiply. Keep param lists near navigators, validate external URLs, and avoid serializing non-JSON-safe values through params. Renaming routes is a cross-cutting change—update analytics, push payloads, and E2E selectors in the same release train.