◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-03-29Intermediate

Local Data Persistence in Rork Apps — Choosing AsyncStorage, MMKV, and SQLite by Measurement

A measured comparison of local storage in Rork apps: AsyncStorage, MMKV, and SQLite. Covers a zero-downtime migration, MMKV pitfalls, and how transactions plus WAL make SQLite bulk inserts dozens of times faster — grounded in hands-on indie development.

Rork585AsyncStorage10MMKV6SQLite3Local StorageData PersistenceReact Native239

✦ Premium Article

The night a list screen froze for a moment

I had added a favorites feature to one of the apps I run as an indie developer. Everything worked smoothly up to a few hundred entries. Then a heavy user's favorites grew into the thousands, and reports came in: every time they opened the list, the screen flashed white for a beat.

The cause was that, on launch, the app loaded the entire favorites list from AsyncStorage and ran JSON.parse on one enormous array every single time. The data was just key-value, yet startup got heavier as the data grew. Back then I had told myself "storage is something I can figure out later," and that complacency came back to me as a visible drop in perceived speed.

Local storage choices don't matter while an app is small — anything works. The difference shows up once a user's data has grown. That's exactly why understanding the mechanics up front saves you from rebuilding later.

This guide compares the three staples available in React Native / Expo-based Rork apps — AsyncStorage, MMKV, and SQLite — and goes deeper than a feature tour: into why speed appears or doesn't, how to migrate safely, the pitfalls to avoid, and how to turn a "slow SQLite" into a fast one. All of it is grounded in things I actually tripped over.

AsyncStorage — The Shortest Path to Something That Works

AsyncStorage is the community-maintained key-value store for React Native. It works out of the box in projects Rork generates, and its learning curve is close to zero.

  • Data format: String key-value pairs (objects via JSON.stringify)
  • API: Fully Promise-based and asynchronous
  • Backend: SQLite on Android (6MB default cap), plist files on iOS
  • Thread safety: Internally serialized, safe under concurrent access
import AsyncStorage from '@react-native-async-storage/async-storage';
 
// Save user preferences
const saveUserPreferences = async (preferences: {
  theme: 'light' | 'dark';
  language: string;
  notifications: boolean;
}) => {
  try {
    const jsonValue = JSON.stringify(preferences);
    await AsyncStorage.setItem('@user_preferences', jsonValue);
    console.log('Preferences saved successfully');
  } catch (error) {
    console.error('Failed to save preferences:', error);
  }
};
 
// Load user preferences
const loadUserPreferences = async () => {
  try {
    const jsonValue = await AsyncStorage.getItem('@user_preferences');
    if (jsonValue !== null) {
      return JSON.parse(jsonValue);
    }
    return { theme: 'light', language: 'en', notifications: true };
  } catch (error) {
    console.error('Failed to load preferences:', error);
    return null;
  }
};
 
// Expected output:
// saveUserPreferences({ theme: 'dark', language: 'en', notifications: true })
// => "Preferences saved successfully"
// loadUserPreferences()
// => { theme: 'dark', language: 'en', notifications: true }

For settings, flags, temporary token storage, a few dozen cached entries, and MVP-stage apps, this is plenty. I personally spend the first few weeks of a new app almost entirely on AsyncStorage — in the phase where I want fast iteration, I'd rather not sink time into storage design.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Why AsyncStorage slows down as data grows, plus a one-time migration to MMKV with no data loss
✦Making SQLite bulk inserts dozens of times faster with transactions and WAL, with measured behavior
✦A quick decision flow to pick AsyncStorage / MMKV / SQLite from the nature of your data
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-06-22
When a Poisoned Cache Crashes Your App on Every Launch — Designing a Safe-Mode Boot Your Users Can Escape On Their Own
When a persisted cache goes bad and the app crashes at the same spot on every launch, the only option left to the user is to reinstall. This article designs a safe-mode boot for Expo (React Native): the app counts its own early crashes, confirms a launch only once it becomes interactive, and resets just the dangerous state in graduated steps.
⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.