React Native MMKV vs AsyncStorage: 30x Faster Storage with Nitro Modules in 2026

react-native-mmkv v4 is now a Nitro Module - synchronous, JSI-backed, and roughly 30x faster than AsyncStorage. This 2026 guide covers how MMKV works, real benchmark numbers, install steps for Expo and bare React Native, encryption, and a complete AsyncStorage migration path.

If your React Native app still leans on AsyncStorage for tokens, user preferences, or cached payloads, every key read is quietly costing you frames. As of 2026, react-native-mmkv v4 is a Nitro Module — fully synchronous, JSI-backed, and benchmarked at roughly 30x faster than AsyncStorage. So, let's dig into how MMKV works under the hood, how it compares to AsyncStorage in real workloads, how to install and configure v4 in Expo or bare React Native, and how to migrate an existing app without losing user data along the way.

Why AsyncStorage Is the Bottleneck You Didn't Notice

@react-native-async-storage/async-storage has been the de facto key/value store for React Native ever since the community fork replaced the in-core module. It works, it's simple, and Expo Go supports it out of the box — but it carries architectural costs that show up the moment your app grows past a handful of keys.

  • Bridge-based and asynchronous. Every getItem and setItem bounces serialized data across the bridge. Even with the new architecture's TurboModules in front of it, the API itself is Promise-based, which forces every consumer into await ladders.
  • Strings only. Booleans, numbers, and objects all need JSON.stringify on the way in and JSON.parse on the way out. That parse cost (multiplied across hydration) tends to dominate real-world usage.
  • SQLite-backed on Android. The Android implementation is literally a SQLite table behind a bridge. For a 1KB read, that's an absurd amount of plumbing.
  • No built-in encryption. Sensitive values like auth tokens have to be wrapped in expo-secure-store or a Keychain helper, which means a second storage system in your app.

None of this is fatal. AsyncStorage is still a perfectly reasonable choice for prototypes or Expo Go workflows. But for any app that does more than a few dozen reads per render, the latency adds up — and the new architecture has made the alternative dramatically easier to adopt.

What MMKV Actually Is

MMKV is a key-value store originally built by WeChat to handle the storage needs of an app with over a billion users. The core C++ library uses memory-mapped files, which means reads and writes happen against a chunk of memory the kernel keeps in sync with disk. There's no SQLite, no JSON parsing, no thread hop. Just memory.

The React Native binding — react-native-mmkv by Marc Rousavy — exposes that core to JavaScript through JSI. As of v4 (rolled out across 2025–2026), the binding is a Nitro Module, which simplified the native layer, made the codebase far easier to contribute to, and shaved overhead off every native call. The latest v4.3.x release is the version you should target in 2026.

What that buys you in practical terms:

  • Synchronous API. storage.getString('token') returns immediately. No Promises. No useEffect dance just to read state on mount.
  • Native primitive support. Strings, numbers, booleans, and ArrayBuffers are first-class. No serialization tax.
  • Built-in AES encryption. Pass an encryptionKey and every read/write is transparently encrypted at the native layer.
  • Multiple instances. You can isolate user-scoped storage from app-global storage with separate IDs and paths.
  • Listener API. Subscribe to changes for any key without rolling your own pub/sub.

Benchmark: MMKV vs AsyncStorage in 2026

The numbers come from mrousavy/StorageBenchmark, which calls a get operation 1,000 times against each storage library on a real device. They've been replicated independently across multiple posts in 2025 and 2026, so they're not a one-off.

OperationAsyncStorageMMKVSpeedup
Single read (avg)2.548 ms0.520 ms~5x
Single write (avg)2.871 ms0.570 ms~5x
1,000 reads (loop)~2,500 ms~120 ms~20x
App-startup hydrationnoticeableimperceptible

Honestly, the bigger story isn't the raw numbers — it's what the synchronous API enables. With AsyncStorage, you can't read state during the first render. You have to render a loading state, kick off an async read, and then re-render. With MMKV, the value is sitting there before the component mounts. That removes a whole class of UI flicker bugs entirely.

Feature Matrix

FeatureAsyncStorageMMKV v4
API styleAsync / PromiseSynchronous
Backing storeSQLite (Android), files (iOS)Memory-mapped files
Native bridgeTurboModuleNitro Module (JSI)
Primitive typesString onlyString, number, boolean, ArrayBuffer
EncryptionNone (use SecureStore separately)Built-in AES
Multiple instancesLimitedFirst-class with IDs and paths
Change listenersNoneaddOnValueChangedListener
Expo Go supportYesNo (requires dev client / prebuild)
Bundle size impactSmaller~200 KB native
Best forPrototypes, Expo GoProduction apps

Installing react-native-mmkv v4

Requirements

  • React Native 0.75 or higher
  • The new architecture enabled (Fabric + TurboModules)
  • react-native-nitro-modules 0.35 or higher
  • For Expo: a dev client build (Expo Go does not bundle MMKV)

Expo (managed workflow with prebuild)

npx expo install react-native-mmkv react-native-nitro-modules
npx expo prebuild --clean
npx expo run:ios
npx expo run:android

Make sure your app.json has the new architecture enabled. With Expo SDK 53+ it's on by default, but verify anyway — I've been burned by silently-disabled flags more than once:

{
  "expo": {
    "newArchEnabled": true,
    "plugins": []
  }
}

Bare React Native

npm install react-native-mmkv react-native-nitro-modules
cd ios && pod install && cd ..

Confirm newArchEnabled=true in android/gradle.properties and RCT_NEW_ARCH_ENABLED=1 in your iOS Podfile environment.

Creating Your First MMKV Instance

Centralize the instance. There's no benefit to spinning up a new MMKV object on every render, and a single shared instance is the recommended pattern.

// storage/index.ts
import { createMMKV } from 'react-native-mmkv'

export const storage = createMMKV()

export const userStorage = createMMKV({
  id: 'user-scoped',
})

export const secureStorage = createMMKV({
  id: 'secure',
  encryptionKey: 'replace-with-keychain-derived-key',
})

Heads up on the v4 change: the constructor is createMMKV(), not new MMKV(). The JS class wrapper was removed in v4 because Nitro exposes the native object directly.

Reading and Writing

import { storage } from './storage'

// Strings
storage.set('username', 'ada')
const username = storage.getString('username') // 'ada'

// Numbers
storage.set('lastSeenAt', Date.now())
const lastSeenAt = storage.getNumber('lastSeenAt')

// Booleans
storage.set('hasOnboarded', true)
const hasOnboarded = storage.getBoolean('hasOnboarded') // true

// Objects (still need stringify, but you skip the parse on null)
const profile = { id: 'u_1', plan: 'pro' }
storage.set('profile', JSON.stringify(profile))
const raw = storage.getString('profile')
const parsed = raw ? JSON.parse(raw) : undefined

// Removing a key — note the v4 rename
storage.remove('username') // was storage.delete in v3

// Clear everything
storage.clearAll()

The delete method got renamed to remove in v4 because delete is reserved in C++ and the Nitro generator exposes native names directly. A small thing, but it'll trip you up if you're copy-pasting from old StackOverflow answers.

Encryption: Securing Tokens at Rest

By default MMKV stores values in plain text on the device's sandboxed filesystem. For most preferences that's fine — iOS and Android already isolate app data — but for auth tokens or refresh secrets, you really should layer on encryption.

import * as Keychain from 'react-native-keychain'
import { createMMKV } from 'react-native-mmkv'

async function getOrCreateStorageKey(): Promise<string> {
  const existing = await Keychain.getGenericPassword({ service: 'mmkv-key' })
  if (existing) return existing.password

  const key = Array.from(
    crypto.getRandomValues(new Uint8Array(16))
  )
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')

  await Keychain.setGenericPassword('mmkv', key, { service: 'mmkv-key' })
  return key
}

export async function createSecureStorage() {
  const key = await getOrCreateStorageKey()
  return createMMKV({ id: 'secure', encryptionKey: key })
}

The pattern is straightforward: derive (or generate-and-store) the encryption key in the OS keychain, then hand it to MMKV. The keychain handles secure-element-backed storage of the key itself; MMKV handles bulk encrypted I/O.

Need to rotate keys? Use recrypt():

secureStorage.recrypt('new-key') // re-encrypts existing data in place
secureStorage.recrypt(undefined) // removes encryption entirely

Reactive Reads with React Hooks

MMKV ships hooks that subscribe to a single key and re-render on change. They feel like useState, except the value is persisted.

import { useMMKVString, useMMKVBoolean, useMMKVNumber } from 'react-native-mmkv'
import { storage } from './storage'

export function ThemeToggle() {
  const [isDark, setIsDark] = useMMKVBoolean('isDark', storage)

  return (
    <Switch value={isDark ?? false} onValueChange={setIsDark} />
  )
}

Behind the scenes the hook uses addOnValueChangedListener, so writes from anywhere in the app — different components, background tasks, even native code — propagate to every subscribed component instantly. Pretty much exactly what you'd want.

Migrating an Existing App from AsyncStorage

For an app that already has user data sitting in AsyncStorage, you can't just swap the import — your users would lose their settings on the next launch. Run a one-time migration on app startup, then keep MMKV as the source of truth.

// storage/migrate.ts
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createMMKV } from 'react-native-mmkv'

export const storage = createMMKV()

const MIGRATION_FLAG = '__migratedFromAsyncStorage__'

export async function migrateFromAsyncStorage(): Promise<void> {
  if (storage.getBoolean(MIGRATION_FLAG)) return

  const keys = await AsyncStorage.getAllKeys()
  for (const key of keys) {
    const value = await AsyncStorage.getItem(key)
    if (value == null) continue

    if (value === 'true' || value === 'false') {
      storage.set(key, value === 'true')
    } else if (!Number.isNaN(Number(value)) && value.trim() !== '') {
      storage.set(key, Number(value))
    } else {
      storage.set(key, value)
    }
  }

  await AsyncStorage.multiRemove(keys)
  storage.set(MIGRATION_FLAG, true)
}

Trigger it once in your root component, before rendering any screen that reads storage:

import { useEffect, useState } from 'react'
import { InteractionManager, View, ActivityIndicator } from 'react-native'
import { migrateFromAsyncStorage } from './storage/migrate'

export default function App() {
  const [ready, setReady] = useState(false)

  useEffect(() => {
    InteractionManager.runAfterInteractions(async () => {
      try {
        await migrateFromAsyncStorage()
      } finally {
        setReady(true)
      }
    })
  }, [])

  if (!ready) {
    return (
      <View style={{ flex: 1, justifyContent: 'center' }}>
        <ActivityIndicator />
      </View>
    )
  }

  return <RootNavigator />
}

A few migration tips, learned the hard way:

  • Run inside InteractionManager.runAfterInteractions so you don't block the splash-to-first-frame transition on slow devices.
  • Coerce types carefully. AsyncStorage stores everything as a string. The snippet above does basic boolean and number detection; for app-specific shapes you'll want a key-aware migrator that knows which keys are JSON, which are numbers, and so on.
  • Keep the flag idempotent. The migration must be safe to run twice. The boolean flag pattern handles this.
  • Ship the AsyncStorage dependency for one release. Don't yank the package the same day you ship the migration — leave it in until your telemetry shows the migration has run for the long tail of users.

Adapter for redux-persist and Zustand

If you persist state with redux-persist or Zustand's persist middleware, you need an adapter. MMKV's synchronous API actually makes this cleaner — no fake Promises required for Zustand, though redux-persist still expects Promise-based methods (so we wrap them).

// redux-persist adapter
import { storage } from './storage'

export const reduxPersistMMKVStorage = {
  setItem: (key: string, value: string) => {
    storage.set(key, value)
    return Promise.resolve(true)
  },
  getItem: (key: string) => {
    return Promise.resolve(storage.getString(key) ?? null)
  },
  removeItem: (key: string) => {
    storage.remove(key)
    return Promise.resolve()
  },
}
// Zustand adapter (synchronous, no Promise wrapping)
import { StateStorage } from 'zustand/middleware'
import { storage } from './storage'

export const zustandMMKVStorage: StateStorage = {
  setItem: (name, value) => storage.set(name, value),
  getItem: (name) => storage.getString(name) ?? null,
  removeItem: (name) => storage.remove(name),
}

Testing with Jest

You don't need to mock MMKV manually — it ships an in-memory mock that activates automatically when Jest or Vitest is detected. Your tests will read and write against a Map under the hood, with the exact same API surface, so most existing tests just keep working after migration.

// jest.config.js — no extra setup needed for MMKV
module.exports = {
  preset: 'jest-expo',
  // ...
}

When You Should Still Reach for AsyncStorage

MMKV is the right answer for the vast majority of production apps in 2026, but there are still a few cases where AsyncStorage makes more sense.

  • You're building inside Expo Go. MMKV is a native module and isn't bundled into Expo Go. If your team's workflow depends on Expo Go (no dev client), AsyncStorage stays.
  • Your app is a hello-world prototype. Don't chase a 30x speedup if you have three keys. Optimize when you've got evidence.
  • Web target without encryption. MMKV's web fallback exists, but encryption is iOS/Android only. If you target web and need encryption, application-level crypto is on you regardless.
  • You're storing megabytes of blobs. MMKV's memory-mapped model is fastest for many small values. For large binary blobs, reach for expo-file-system or SQLite instead.

Common Pitfalls

  • Forgetting to enable the new architecture. v4 requires it. If your build silently fails at runtime, check newArchEnabled first.
  • Using storage.delete from v3 docs. It's storage.remove in v4. The error is loud, but the symptom can look like a silently failing reset flow.
  • Storing huge JSON blobs. If you're shoving entire query caches into one key, you'll lose the speedup advantage and bloat memory. Split keys, or move to SQLite for structured data.
  • Hard-coding the encryption key. A constant baked into your bundle is cosmetic, not security. Always derive the key through Keychain or a secure-element-backed source.
  • Migration without a version flag. If your migration script runs on every cold start, you'll silently overwrite later writes with older AsyncStorage values. Always gate the migration with a flag MMKV itself stores.

FAQ

Is MMKV really 30x faster than AsyncStorage?

For repeated reads of small values — exactly the workload most apps care about — yes. The single-call latency is roughly 5x lower, and for tight loops or hydration paths the cumulative speedup compounds to 20–30x in published benchmarks. Whether you'll notice the speedup depends on your workload: an app reading 5 keys at startup won't feel different, but an app hydrating 200 settings absolutely will.

Does MMKV work with Expo?

Yes — but not inside Expo Go. You'll need a development build (npx expo prebuild followed by expo run:ios or expo run:android, or an EAS Build dev client). Once you have a dev client, MMKV works exactly the same as in any other project.

Is MMKV secure for storing auth tokens?

MMKV's built-in AES encryption is fine for the bulk-of-data layer, but the encryption key itself must be stored in the OS keychain (iOS Keychain or Android Keystore via react-native-keychain or expo-secure-store). For very sensitive secrets — biometric-gated tokens, payment credentials — keep using a dedicated secure store. For everything else, MMKV with a keychain-backed key is solid.

Can I use MMKV with redux-persist or Zustand?

Both. redux-persist needs a thin Promise wrapper around MMKV's synchronous methods because its API is Promise-based. Zustand's persist middleware accepts synchronous storage directly, so the adapter is even simpler. Adapter snippets are shown above.

Should I migrate my existing app, or only use MMKV for new code?

If your AsyncStorage usage is small and isolated, just do a one-time migration — it pays for itself in cold-start performance and gives you encryption for free. If your app has hundreds of scattered AsyncStorage calls, migrate gradually by category (auth, preferences, cache) behind a small abstraction so you can swap implementations safely. Avoid running both stores in parallel for the same key — that path makes debugging stale data nearly impossible.

Does MMKV support listeners or change events?

Yes. storage.addOnValueChangedListener((key) => {...}) fires whenever any key in that instance changes. The hooks (useMMKVString, useMMKVBoolean, etc.) are built on top of this, so updates from anywhere in the app — different screens, background tasks, even native code — propagate automatically.

Wrapping Up

For React Native apps shipping in 2026, MMKV is no longer the "advanced option" — it's the sensible default for production storage. The v4 Nitro Module rewrite removed the last rough edges, the synchronous API simplifies render code, and the built-in encryption replaces a separate dependency for sensitive data. AsyncStorage still has a niche in Expo Go and hello-world apps, but if you're past the prototype stage, the migration is straightforward and the performance is genuinely worth it.

My recommendation: start with one key — the auth token, or the user-preferences blob — measure the difference on a low-end device, and expand from there. Most teams complete the migration in a single afternoon, and most users will never know it happened. Except that their app feels a little snappier on cold start, which is the whole point.

About the Author Editorial Team

Our team of expert writers and editors.