This comparison evaluates six distinct approaches to local data persistence in JavaScript: levelup (a wrapper for the high-performance LevelDB), localforage (an asynchronous wrapper for browser storage APIs), lowdb (a simple JSON file database), nedb (an embedded MongoDB-like engine), node-persist (a synchronous JSON store for Node.js), and pouchdb (a browser-syncable NoSQL database). While all solve the problem of storing data locally, they differ fundamentally in their storage engines, synchronization capabilities, API styles (sync vs. async), and target environments (Node.js vs. Browser vs. Both).
When building JavaScript applications, knowing where and how to store data locally is a critical architectural decision. The ecosystem offers tools ranging from simple JSON file writers to full-blown NoSQL engines with sync capabilities. Let's break down how levelup, localforage, lowdb, nedb, node-persist, and pouchdb handle storage, querying, and environment compatibility.
nedbBefore diving into features, we must address nedb. This package is deprecated and no longer maintained. Its repository has been archived, and it contains known security vulnerabilities that will not be fixed.
// ❌ DO NOT USE IN NEW PROJECTS
// const Nedb = require('nedb');
// This pattern is obsolete and unsafe.
If you need an embedded document store with a MongoDB-like API, you should migrate to pouchdb (for browser/Node sync) or consider modern alternatives like better-sqlite3 for relational needs. The rest of this comparison focuses on actively maintained solutions.
The fundamental difference lies in how data is structured and stored on disk or in the browser.
levelup provides a raw key-value store backed by LevelDB (or LMDB). It stores data as sorted strings or buffers. You manage serialization yourself.
// levelup: Raw key-value storage
const level = require('levelup');
const db = await level('./my-db');
// You must stringify objects manually
await db.put('user:1', JSON.stringify({ name: 'Alice', age: 30 }));
const data = await db.get('user:1');
const user = JSON.parse(data); // Manual parsing required
localforage abstracts browser storage (IndexedDB, WebSQL, localStorage) into a simple key-value API. It handles serialization automatically.
// localforage: Browser key-value with auto-serialization
import localforage from 'localforage';
// Automatically serializes the object
await localforage.setItem('user:1', { name: 'Alice', age: 30 });
const user = await localforage.getItem('user:1'); // Returns object directly
lowdb stores the entire database as a single plain JSON file on the filesystem. It loads the whole file into memory on every read.
// lowdb: Single JSON file storage
import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';
const db = new Low(new JSONFile('db.json'), { users: [] });
await db.read();
// Direct mutation of the in-memory object
db.data.users.push({ name: 'Alice', age: 30 });
await db.write(); // Writes the entire JSON file back to disk
node-persist also uses a JSON file per key (or a single directory of files) but operates synchronously by default, blocking the Node.js event loop.
// node-persist: Synchronous file-based storage
const storage = require('node-persist');
await storage.init({ dir: 'data' });
// Synchronous write (blocks event loop)
storage.setItemSync('user:1', { name: 'Alice', age: 30 });
const user = storage.getItemSync('user:1');
pouchdb is a document-oriented database. It stores JSON documents with unique IDs and maintains internal indexes (B-trees) for efficient querying.
// pouchdb: Document store with internal indexing
import PouchDB from 'pouchdb';
const db = new PouchDB('my_database');
// Documents must have an _id
await db.put({ _id: 'user:1', name: 'Alice', age: 30 });
const doc = await db.get('user:1'); // Returns full document object
How you retrieve data varies wildly between these tools.
levelup, localforage, and node-persist are primarily key-value stores. They excel at fetching data by a specific key but lack built-in query engines for filtering by value.
// levelup: Stream keys to find matches (Manual filtering)
const stream = db.createKeyStream({ gte: 'user:', lte: 'user:\uffff' });
stream.on('data', (key) => {
// You must fetch and parse each value to check properties
});
// localforage: No native query support
// You must fetch all keys and iterate
const keys = await localforage.keys();
// Manual loop required to find specific users
lowdb relies on Lodash for querying the in-memory array. It is flexible but requires loading all data first.
// lowdb: Query via Lodash chain
import { Low } from 'lowdb';
// ... setup db ...
// Find users over 25
const adults = db.data.users.filter(u => u.age > 25);
pouchdb includes a powerful built-in query system (MapReduce or Mango indexes) that runs efficiently without loading all data into memory.
// pouchdb: Create an index
await db.createIndex({
index: { fields: ['age'] }
});
// Efficient query using the index
const result = await db.find({
selector: { age: { $gt: 25 } }
});
This is the biggest differentiator for modern web apps.
pouchdb is unique in this list because it supports bidirectional replication with CouchDB. This allows true "offline-first" architectures.
// pouchdb: Two-way sync with remote server
const remoteDB = new PouchDB('https://my-server.com/db');
const localDB = new PouchDB('local_db');
// Syncs changes automatically in both directions
localDB.sync(remoteDB, {
live: true,
retry: true
});
levelup, localforage, lowdb, and node-persist are strictly local. If you need sync, you must build the logic yourself (e.g., sending API requests on reconnect).
// localforage: Manual sync implementation required
async function syncData() {
const localData = await localforage.getItem('cache');
// You must write custom fetch/put logic to send to server
await fetch('/api/sync', { method: 'POST', body: JSON.stringify(localData) });
}
The way these libraries handle I/O impacts application responsiveness.
node-persist is synchronous. While simple, it blocks the Node.js event loop during disk operations, which can freeze your server under load.
// node-persist: Blocking operation
// The server cannot handle other requests during this write
storage.setItemSync('large-key', hugeDataObject);
levelup, localforage, lowdb (v2+), and pouchdb are asynchronous. They use Promises or callbacks to avoid blocking.
// levelup: Non-blocking async write
await db.put('key', 'value');
// Event loop remains free to handle other requests
// lowdb: Async write (v2+)
await db.write();
levelup generally offers the highest raw throughput for write-heavy workloads due to its Log-Structured Merge-tree (LSM) architecture, making it suitable for high-performance Node.js backends.
localforage is optimized for the browser, automatically choosing the fastest available backend (usually IndexedDB) while providing a consistent API.
Where can you run these?
levelup, lowdb, node-persist, nedb (deprecated).localforage (though it has a Node shim, it's designed for browsers).pouchdb.// pouchdb: Same code runs in Node and Browser
import PouchDB from 'pouchdb';
// Works in Chrome, Firefox, Safari, and Node.js
const db = new PouchDB('my_data');
| Feature | levelup | localforage | lowdb | node-persist | pouchdb |
|---|---|---|---|---|---|
| Data Model | Key-Value | Key-Value | JSON File | Key-Value | Document (NoSQL) |
| Query Engine | None (Manual) | None (Manual) | Lodash (In-memory) | None (Manual) | MapReduce / Mango |
| Sync Ready | ❌ No | ❌ No | ❌ No | ❌ No | ✅ Yes (CouchDB) |
| I/O Model | Async | Async | Async | Sync (Blocking) | Async |
| Environment | Node | Browser | Node | Node | Universal |
| Best For | High perf, custom engines | Simple browser cache | CLI tools, prototypes | Simple Node scripts | Offline-first apps |
Choosing the right tool depends entirely on your constraints:
Building an Offline-First Web App?
There is only one choice: pouchdb. Its ability to sync with CouchDB saves months of custom engineering. The trade-off is bundle size and complexity.
Need a Simple Cache in the Browser?
Use localforage. It gives you a clean Promise API without forcing you to deal with the quirks of IndexedDB directly.
Writing a CLI Tool or Small Node Script?
lowdb is fantastic. Storing state as a human-readable JSON file makes debugging trivial. Just ensure your data set stays small.
Building a High-Performance Node Backend?
levelup provides the raw speed and durability of LevelDB. It requires more code to manage schemas and queries, but it scales incredibly well.
Avoid at All Costs:
nedb is dead. node-persist should be avoided for any server handling concurrent requests due to its blocking nature.
By matching the tool to your specific data access patterns and environment, you can avoid over-engineering while ensuring your application remains robust and maintainable.
Choose localforage if you are building a web application that needs a simple, asynchronous key-value store in the browser without worrying about the underlying implementation (IndexedDB, WebSQL, or localStorage). It is perfect for caching user preferences, offline data, or session state where you want a clean Promise-based API. Do not use this for complex queries or if you need data to sync with a remote server automatically.
Choose levelup when you need maximum performance and fine-grained control over key-value storage in a Node.js environment. It is ideal for building custom database layers, caching systems, or handling large datasets where direct access to the underlying LevelDB/LMDB engine is required. Avoid this if you need a ready-made query language or browser support without significant extra tooling.
Choose lowdb for small-scale Node.js projects, CLI tools, or prototypes where you need a tiny, full-featured database stored as a plain JSON file. It is excellent when you want to easily inspect and edit data manually and don't expect high concurrency or massive write volumes. Avoid it for production web servers with heavy traffic due to its synchronous file locking limitations.
Do NOT choose nedb for new projects. It is officially deprecated and no longer maintained. While it offers a familiar MongoDB-like API for embedded use, critical security vulnerabilities and lack of updates make it unsafe. Instead, evaluate pouchdb for similar query capabilities or lowdb for simple JSON storage.
Choose node-persist when you need an extremely simple, synchronous key-value store for Node.js scripts where performance is not critical and data is small. It is useful for caching build artifacts or storing simple configuration states in CLI tools. Avoid it for web servers or applications requiring asynchronous non-blocking I/O, as it blocks the event loop during read/write operations.
Choose pouchdb if your application requires offline-first capabilities with automatic bidirectional synchronization to a CouchDB server. It is the definitive choice for progressive web apps (PWAs) or mobile hybrids that must work seamlessly without internet and sync changes when connectivity returns. Be aware that it adds significant bundle size and complexity if you do not need the sync feature.
localForage is a fast and simple storage library for JavaScript. localForage
improves the offline experience of your web app by using asynchronous storage
(IndexedDB or WebSQL) with a simple, localStorage-like API.
localForage uses localStorage in browsers with no IndexedDB or WebSQL support. See the wiki for detailed compatibility info.
To use localForage, just drop a single JavaScript file into your page:
<script src="localforage/dist/localforage.js"></script>
<script>localforage.getItem('something', myCallback);</script>
Try the live example.
Download the latest localForage from GitHub, or install with npm:
npm install localforage
Lost? Need help? Try the localForage API documentation. localForage API文档也有中文版。
If you're having trouble using the library, running the tests, or want to contribute to localForage, please look through the existing issues for your problem first before creating a new one. If you still need help, feel free to file an issue.
Because localForage uses async storage, it has an async API. It's otherwise exactly the same as the localStorage API.
localForage has a dual API that allows you to either use Node-style callbacks or Promises. If you are unsure which one is right for you, it's recommended to use Promises.
Here's an example of the Node-style callback form:
localforage.setItem('key', 'value', function (err) {
// if err is non-null, we got an error
localforage.getItem('key', function (err, value) {
// if err is non-null, we got an error. otherwise, value is the value
});
});
And the Promise form:
localforage.setItem('key', 'value').then(function () {
return localforage.getItem('key');
}).then(function (value) {
// we got our value
}).catch(function (err) {
// we got an error
});
Or, use async/await:
try {
const value = await localforage.getItem('somekey');
// This code runs once the value has been loaded
// from the offline store.
console.log(value);
} catch (err) {
// This code runs if there were any errors.
console.log(err);
}
For more examples, please visit the API docs.
You can store any type in localForage; you aren't limited to strings like in
localStorage. Even if localStorage is your storage backend, localForage
automatically does JSON.parse() and JSON.stringify() when getting/setting
values.
localForage supports storing all native JS objects that can be serialized to JSON, as well as ArrayBuffers, Blobs, and TypedArrays. Check the API docs for a full list of types supported by localForage.
All types are supported in every storage backend, though storage limits in localStorage make storing many large Blobs impossible.
You can set database information with the config() method.
Available options are driver, name, storeName, version, size, and
description.
Example:
localforage.config({
driver : localforage.WEBSQL, // Force WebSQL; same as using setDriver()
name : 'myApp',
version : 1.0,
size : 4980736, // Size of database, in bytes. WebSQL-only for now.
storeName : 'keyvaluepairs', // Should be alphanumeric, with underscores.
description : 'some description'
});
Note: you must call config() before you interact with your data. This
means calling config() before using getItem(), setItem(), removeItem(),
clear(), key(), keys() or length().
You can create multiple instances of localForage that point to different stores
using createInstance. All the configuration options used by
config are supported.
var store = localforage.createInstance({
name: "nameHere"
});
var otherStore = localforage.createInstance({
name: "otherName"
});
// Setting the key on one of these doesn't affect the other.
store.setItem("key", "value");
otherStore.setItem("key", "value2");
You can use localForage with RequireJS:
define(['localforage'], function(localforage) {
// As a callback:
localforage.setItem('mykey', 'myvalue', console.log);
// With a Promise:
localforage.setItem('mykey', 'myvalue').then(console.log);
});
If you have the allowSyntheticDefaultImports compiler option set to true in your tsconfig.json (supported in TypeScript v1.8+), you should use:
import localForage from "localforage";
Otherwise you should use one of the following:
import * as localForage from "localforage";
// or, in case that the typescript version that you are using
// doesn't support ES6 style imports for UMD modules like localForage
import localForage = require("localforage");
If you use a framework listed, there's a localForage storage driver for the models in your framework so you can store data offline with localForage. We have drivers for the following frameworks:
If you have a driver you'd like listed, please open an issue to have it added to this list.
You can create your own driver if you want; see the
defineDriver API docs.
There is a list of custom drivers on the wiki.
You'll need node/npm and bower.
To work on localForage, you should start by
forking it and installing its
dependencies. Replace USERNAME with your GitHub username and run the
following:
# Install bower globally if you don't have it:
npm install -g bower
# Replace USERNAME with your GitHub username:
git clone [email protected]:USERNAME/localForage.git
cd localForage
npm install
bower install
Omitting the bower dependencies will cause the tests to fail!
You need PhantomJS installed to run local tests. Run npm test (or,
directly: grunt test). Your code must also pass the
linter.
localForage is designed to run in the browser, so the tests explicitly require a browser environment. Local tests are run on a headless WebKit (using PhantomJS).
When you submit a pull request, tests will be run against all browsers that localForage supports on Travis CI using Sauce Labs.
As of version 1.7.3 the payload added to your app is rather small. Served using gzip compression, localForage will add less than 10k to your total bundle size:
This program is free software; it is distributed under an Apache License.
Copyright (c) 2013-2016 Mozilla (Contributors).