v9.0.0-beta.0 - Unified Storage Model
Summary
🎁 Features
- There is no longer a
useMemstore
flag. Replicache responds is near-memory speed all the time and persists to IndexedDB in the background (see Unified Storage Model below). - Replicache is faster, often dramatically so, on every benchmark as compared to v8 (see Performance below).
- Replicache now has an experimental new poke method. This enables directly programmatically adding data to Replicache without having to go through pull.
- The mutation type sent to
replicache-push
now includes atimestamp
property, which is the original (client-local) timestamp the mutation occurred at. - The size of
replicache.min.mjs.br
was reduced 28%, down to ~18kb.
🧰 Fixes
- Replicache is no longer slow when dev tools is open (#634)
⚠️ Breaking Changes
- The
name
parameter is now required. This is used to differentiate Replicache data within the same origin. For security, provide a value that includes a unique ID for the current user. This ensures each user sees and modifies only their own local data.
Unified Storage Model
The major change in Replicache v9 is the introduction of our new Unified Storage Model.
This removes the useMemstore
flag from the Replicache constructor, because it’s no longer needed: Replicache now provides read/write access at near-memory speed, suitable for use in interactive applications like drag/drop and typing, but also saves snapshots to IDB every few seconds.
Background
In Replicache v8, there were two storage modes: memory, and persistent, controlled by the useMemstore
constructor flag.
In persistent mode (useMemstore=false
), each browser profile was a Replicache client, with a single clientID
and storage area shared amongst all tabs over the lifetime of the profile. Accessing data directly from IDB is super slow — way too slow to back interactive experiences like mouse movement and typing — which forced developers to cache this data in memory on top of Replicache. This in turn created complexities keeping the in-memory and persistent state in sync. Additionally sharing a single storage area among many tabs created complexities versioning this storage — you can’t change the schema of storage that other tabs are using!
In contrast, in memory mode (useMemstore=true
), each unique instance of the Replicache
class was its own client, with its own unique clientID
and in-memory storage that only lasted the lifetime of that instance (usually a single page load). Being in memory, this mode was much faster and could back mouse movement and keystrokes, but was only suitable for small amounts of data since you wouldn’t want to re-download tons of data on every startup!
Unified Storage Model
Starting in Replicache v9, useMemstore
goes away and there is only one unified storage model that mostly combines the best attributes of the old memory mode and persistent mode: it’s as fast (actually faster in most cases — see PERF) than the old memory mode, but also persists every few seconds to storage so that data can be reused across instances.
Just like the old memory model, every instance of the Replicache
class (again, every individual page load) is its own unique client with its own unique clientID
. And conceptually each such client has its own distinct storage area, separate from all other clients.
💡 Note
Internally, we heavily deduplicate storage amongst clients, so that in reality each client only stores what is unique to it.
When a new client is instantiated, Replicache forks the storage from some previous instance with the same name
and schemaVersion
(see schema versioning, below), so that the net effect is almost as if the storage was shared between the two tabs.
Importantly, though, changes in one tab do not show up immediately in other tabs because they don’t completely share storage. When online, it will appear as if storage is shared because changes in one tab will be synced rapidly to other tabs via the server. But when offline, that syncing will stop occurring and the tabs will proceed independently (see offline, below).
Versioning
A previous headache in persistent mode was versioning the local schema. We could not use the common strategy of migrating the schema on startup since other tabs might be using the storage at that moment. Also, writing migration code is difficult to do correctly and not a task our users reported being excited about.
With each client having its own logical storage, things are far simpler:
- When you construct Replicache, optionally provide a
schemaVersion
which is the version of the data understood by the calling application code. - When you change the format of the client view in a backward incompatible way, change the schema version.
- When Replicache forks to create a new storage area, it only forks from previous clients with the same
schemaVersion
. This does mean that when you change your schema version, clients will have to download a new copy of the data. But this is much more robust than trying to migrate data, and we think it’s the right tradeoff for almost all apps. - Other clients that haven’t yet upgraded proceed happily using the old schema in their own storage until they decide to upgrade.
- Replicache also includes the
schemaVersion
inreplicache-push
andreplicache-pull
so that the server can respond appropriately.
Offline Support
In the old persistent model, Replicache’s offline features were simple to understand: all the data was stored locally first in one profile-wide storage area, then synced to the server. Thus, Replicache apps would transition perfectly well between online and offline, tabs would appear to sync with each other while offline, and apps could even start up offline (provided developers used e.g., ServiceWorker properly to enable that).
Part of the tradeoff for getting faster performance is that Replicache’s offline-support is no longer quite as simple or robust.
Specifically:
- As with v8, a Replicache tab that is running online can go offline and continue working smoothly for some time (~hours to days depending on frequency of writes).
- As with v8, Replicache saves changes locally every few seconds. Offline tabs can be switched away from or closed, and the computer can even shut down or crash without changes being lost. Any work done offline will be pushed to the server the next time the app is online using Replicache’s normal conflict resolution.
- Unlike v8, when offline, tabs do not sync with each other. Each proceeds independently until the network is restored. Note that this also means that if a tab is closed offline, then a new tab opened offline, the new tab will not see the changes from the first tab until the network is restored.
We call this concept Local Acceleration, as opposed to Offline-First. In practice most modern web applications are not intended to be used for long periods offline, and can’t startup offline anyway. Local Acceleration captures the key benefits of offline-first for most applications — instant responsiveness and resilience against short periods of network loss — while optimizing for optimal online performance.
⚠️ Warning:
In this v9.0.0-beta.0 we don’t yet recover mutations from tabs closed while offline. This will be in the next beta.
Transitioning to v9
Despite the above lengthy explanation, the transition to v9 should be fairly seamless. Basically:
- Remove
useMemstore
from yourReplicache
constructor if present. - Ensure you provide a
name
parameter toReplicache
, this is now required (generally the userID that is logged in). - Do not use the
clientID
as a parameter to generate the diff forreplicache-pull
from. Only thecookie
should be used. This is because when Replicache forks to create a new client, it assigns a new clientID. If you are using theclientID
as an input toreplicache-pull
you will find that in many cases theclientID
is new and thus probably send reset patches to every new client.
Performance
Metric | v9 | v8 (persistent) | v8 (mem) |
---|---|---|---|
Write/Sub/Read (1mb total storage) | 1.6ms | 72ms (+45x) | 2.8ms (+1.75x) |
Write/Sub/Read (16mb total storage) | 3.8ms | 267ms (+70x) | 5.4ms (+1.4x) |
Bulk Populate 1mb | 45ms | 183ms (+4x) | 108ms (+2.4x) |
Scan 1mb | 3.1ms | 77ms (+25x) | 3.7ms (+1.2x) |
Create Index (5mb total storage) | 240ms | 1150ms (+4.8x) | 300ms (+1.25x) |