NativeScope / Storage
Large datasets
Why a million keys and multi-gigabyte databases stay responsive, and what the guarantees actually are.
On this page
Devtools usually fall over on real data. They hold every value in memory and send whole tables across the wire. NativeScope is built on a narrower, testable rule instead:
Values, rows, wire messages and rendered DOM stay bounded by a page, chunk, or viewport.
Rendering is O(viewport), table and value transfers are O(page or chunk), and only the selected
instance remains cached in the Studio. Key names are the honest exception: AsyncStorage and MMKV
expose their inventory through getAllKeys(), so those provider APIs necessarily materialize the
list of names on the device. NativeScope pages the values and previews that follow; it does not
pretend the underlying key enumeration is free.
What this means in practice
Key values are cursor-paginated. The runtime enumerates key names, orders them deterministically, then reads values only for the requested page. The studio asks for the page after a given key rather than an offset, so pages stay stable while your app writes and deletes underneath. Scrolling retains the pages you have visited in the active instance so you can move back without another round trip; switching instances releases that cache.
Large values arrive as a preview. A value over 64 KB is delivered truncated, with its real size reported. The complete value streams on demand in chunks with a checksum, so nothing on the wire is ever big enough to stall the UI.
Editing is blocked until the value is fully loaded. This is the part that matters most. A preview can never be written back over real data — the save button stays disabled, and operations that would need a complete value (duplicating a key, restoring a snapshot) refuse to run on a truncated one rather than silently corrupting it.
Search runs on the device. Searching a million keys does not transfer a million keys. The scan happens where the data is and only matches come back.
Export streams when the browser supports direct file writing. In Chromium, exporting a table or instance writes NDJSON to disk chunk by chunk. Browsers without the File System Access API fall back to a Blob download, which necessarily retains the export in browser memory; the UI identifies this fallback before large exports become a surprise.
SQLite counts are exact until exactness gets expensive. A cheap upper bound on the table's size
decides: below the budget the count is computed exactly and rendered right away, so an ordinary table
never shows an approximation. Above it, an estimate renders instantly, prefixed with ~, and the exact
COUNT(*) lands from the background. You never wait on COUNT(*) over a hundred million rows to see
a table, and you never read ~ 20026 off a table that holds fourteen rows.
A view has no such upper bound to read — no metadata at all, and its COUNT(*) may materialize
a whole JOIN. So the budget moves inside the query: the limit sits in the subquery, which caps the
work by construction instead of measuring first. Below the budget the count is exact, above it the
~ and the background pass behave as they do for a large table. The details are in
SQL views.
BLOBs are read-only, and sized. A binary column reads as (blob, 7.8 KB) — the real size, even
though only the first 3 KB travel with the page. Double-click opens a hex/ASCII viewer that can stream
the complete value and copy it as base64. There is no write path: the row grid never opens a text
editor over binary, because saving that text would replace the bytes.
Why the JS thread stays free
Reading a page of values is done in small batches, and the runtime yields to the event loop between
them. Streaming, searching and exporting use the same cooperative pattern where the provider API
allows it. The hard guarantee is that value reads and wire payloads are bounded; key-name enumeration
still inherits the cost of each storage library's getAllKeys() primitive.
Bursts of writes are coalesced with a leading-and-trailing window, so a loop writing the same key a thousand times a second produces a steady trickle of events carrying counts, not a thousand renders. If the pending queue ever hits its cap, it flushes immediately rather than growing.
Deleting at scale
Deletion never costs one round-trip per row.
Selected rows go out as a single command and run inside a transaction on the device, chunked to stay under SQLite's variable limit. Deleting 1,200 selected rows is 3 statements, not 1,200 commands. Because it is one transaction, a failure part-way through rolls back — you never end up with half a deletion.
Emptying a table is a single DELETE FROM with no WHERE, which is the form where SQLite applies
its truncate optimization and discards pages instead of walking rows. In the synthetic dataset,
100,001 rows are removed in under a second, and the cost barely moves with row count. The eraser
button in the table toolbar does this; it asks you to type the table name first, and it cannot be
undone — there is no snapshot of a million rows to restore.
Neither is offered on a view, and that is a deliberate refusal rather than a missing feature.
The truncate optimization does not apply to a view, so the same DELETE FROM fires its
INSTEAD OF trigger once per row — on a sync engine's database that means one queued upload per
row, sent to a production server. SQL views explains what stays available
instead.
Undo is offered for deletions up to 200 rows. Above that it would re-insert row by row and hold every cell in memory, so it is declined explicitly rather than offered and then stalling.
The guarantee about completeness
Nothing above hides data from you. Every truncation is announced, sized, and has a path to the complete value. A preview is a rendering decision, never a limit on what you can reach.
Trying it
The CLI ships a synthetic dataset so you can see the behaviour without a real app.
npx nativescope --fake --fake-scaleyarn nativescope --fake --fake-scalepnpm nativescope --fake --fake-scalebunx nativescope --fake --fake-scaleThat connects a simulated runtime with thousands of keys, multi-megabyte values, and a hundred-thousand-row SQLite table.

