Capacitor Persistent Storage: Native Storage Plugin vs localStorage Decision Guide
Choose native storage for persistent settings and tokens in Capacitor apps, and localStorage only for ephemeral web-only state. Compare persistence, threading, size and bundle impact.
31 Jul 2025, 17:25 UTC

The decision you actually face
You need small, persistent key-value data in an Ionic Capacitor app: user preferences, auth tokens, feature flags. The practical choice is between HTML5 localStorage in the web view and a native-backed store like @capacitor-community/storage. The takeaway is simple: use native storage for anything you must keep across web view clears and app updates; use localStorage only for ephemeral, web-only state where zero native dependency matters.
Constraints that drive the choice
Persistence boundary. localStorage lives inside the web view. Clearing site data, reinstalling the app, or a web view cache purge removes it. Native storage is backed by SharedPreferences on Android and NSUserDefaults on iOS, and falls back to localStorage on the web. It survives web view clears.
Threading and UI impact. localStorage is synchronous. Heavy reads/writes block the UI thread. Native storage is asynchronous via Capacitor’s bridge, so it does not block rendering.
Size and type. Both stores are string based. Native storage plugins typically serialize objects to JSON for you. localStorage requires manual JSON.stringify/parse. NSUserDefaults is not designed for large binary blobs; values larger than about 1 MB should go to Capacitor Filesystem instead.
Bundle and dependency cost. localStorage adds nothing. Native storage adds a small native dependency, roughly tens of kilobytes, and requires a Capacitor sync step.
Option comparison
| Factor | HTML5 localStorage | Native Storage via Capacitor |
|---|---|---|
| API | Synchronous, window.localStorage.getItem/setItem | Asynchronous, Promise based via Capacitor bridge |
| Platform backing | Web view only | SharedPreferences Android, NSUserDefaults iOS, localStorage fallback on web |
| Persistence | Lost on web view data clear, site data clear, reinstall | Persists across web view clears and app updates |
| Typical quota | ~5 MB per origin | Limited by OS preferences store, not suitable for large files |
| Latency | Sub-millisecond, but synchronous | Low single-digit ms via bridge, non-blocking |
| Bundle impact | None | Small native plugin + JS wrapper |
Trade-offs in practice
Choose native storage when the data must survive user actions that clear web view data, when you need thread-safe writes on mobile, or when you want consistent behavior across native and web builds. It is the right default for tokens, onboarding flags, and settings.
Choose localStorage when you are prototyping, building a web-only feature, or storing truly ephemeral UI state that can be recreated. The synchronous API is fine for tiny reads on page load, but avoid large loops of set/get.
Limitations to remember. Clearing app data via device settings erases native storage just like localStorage. Neither option provides backup across device reset. Plugin version must match your Capacitor major version; mismatched versions can cause runtime errors on native sync. On iOS, NSUserDefaults is not a file store. For files >1 MB use Capacitor Filesystem.
Concrete implementation pattern
Install the community storage plugin at project root. This adds the native code and JS wrapper.
npm install @capacitor-community/storage
npx cap syncRun the commands in the terminal at the project root. No special permissions are required. After sync, the native projects are updated.
Example usage in TypeScript. The plugin exposes set and get with a key/value shape. Values are serialized automatically.
import { Storage } from '@capacitor-community/storage';
async function savePreference(key: string, value: unknown) {
await Storage.set({ key, value: JSON.stringify(value) });
}
async function loadPreference<T>(key: string): Promise<T | null> {
const { value } = await Storage.get({ key });
return value ? JSON.parse(value) as T : null;
}For localStorage the equivalent is synchronous and manual:
function saveLocal(key: string, value: unknown) {
localStorage.setItem(key, JSON.stringify(value));
}
function loadLocal(key: string) {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
}How to validate the persistence boundary
Set a test key with both stores, then clear web view data.
- Write a value with Storage.set and with localStorage.setItem.
- In Chrome DevTools for the web view, open Application → Clear storage and click Clear site data.
- Reload the app and read both keys.
Expected behavior: the native-backed value remains, the localStorage value is gone. This confirms the persistence boundary without claiming a specific test run.
Latency check. You can measure 100 sequential set/get operations with performance.now() to compare typical bridge overhead versus synchronous access. Native storage is non-blocking but involves bridge latency; localStorage is faster per op but can spike near quota.
Decision checklist
- Data must survive web view clears → native storage
- Data is small, ephemeral, web-only → localStorage acceptable
- Large blobs or files → Capacitor Filesystem, not either store
- Need consistent API across iOS, Android, web → native storage with web fallback
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.