Skip to content

perf: Nitro V1 with optimized result encoding - #468

Draft
chrispader wants to merge 22 commits into
v1from
v1-optimized
Draft

chrispader wants to merge 22 commits into
v1from
v1-optimized

Conversation

@chrispader

Copy link
Copy Markdown
Member

The Nitro V1 stack was slower than main in some device benchmarks, mostly because of the AnyJSON result encoding from #467. Apple builds format REAL values with SQLite's slower number formatter, and every result pays for a JSON parse, including empty and single-row results.

This PR combines the whole stack with fixes for both, two smaller optimizations, and benchmark cases for asynchronous reads and fractional numbers, so everything can be measured in one build. V1_OPTIMIZATIONS.md lists each change and the stack PR it belongs to.

🤖 Generated with Claude Code

chrispader and others added 19 commits October 23, 2026 01:16
The example only used react-native-quick-crypto and
@craftzdog/react-native-buffer to provide a global Buffer. The buffer
package covers that without native code and drops a Nitro 0.x module
that can't build against Nitro V1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rewrite the Nitro specs as decorated classes, regenerate the bindings,
and port the C++ hybrid objects to V1's value-type Promise, ArrayBuffer
and Callback APIs. JavaScript imports the hybrid classes from the specs,
which re-export the runtime modules that nitrogen generates under

Nitro V1 doesn't support CocoaPods, so the iOS, macOS and native
lockfile jobs are paused. The Nitro 0.37 patch moves into the macOS
example because the root install can't apply it to Nitro V1.

BREAKING CHANGE: Requires React Native 0.88 and Nitro V1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

Co-authored-by: Codex <[email protected]>
The podspec loads CocoaPods files that nitrogen V1 no longer generates,
so the test can't run until iOS builds with Swift Package Manager.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The package's react-native and source fields pointed at src, so apps
compiled the decorated .nitro.ts specs with their own Babel config,
which React Native's default preset can't parse. The entry points now
point at lib, and bob parses the specs through per-target Babel
configs. main points at the CommonJS build that bob validates it
against, and CI builds the package so a broken build shows up before
a release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Nitro V1 only builds iOS through Swift Package Manager. Package.swift
manifests now build the library and sqlite-vec. The library reads the
app's nitroSQLite settings like the podspec did and adds iosSqliteFlags
for custom SQLite definitions, and sqlite-vec registers itself when
SwiftPM links it. The podspecs remain so React Native CLI can discover
the packages.

The example app moves to Swift Package Manager with patched manifests
for react-native-screens and the clipboard module, and iOS CI resumes
on Xcode 27.

BREAKING CHANGE: iOS apps must build with Swift Package Manager and
Xcode with Swift 6.4 or newer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Update the installation steps, the iOS configuration page and the
encryption guide for Nitro V1 and Swift Package Manager, including the
nitroSQLite.iosSqliteFlags setting and the manifest cache that applies
package.json changes only after it's cleared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Nitro V1 registers hybrid objects under the module's namespace, so
NitroSQLitePreparedStatement no longer needs its prefix to stay
unique. The spec, its C++ implementation and the generated bindings
now use PreparedStatement. The public NitroSQLitePreparedStatement
type keeps its name because the JavaScript API already exports a
PreparedStatement interface.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shorter class name changes how clang-format wraps the constructor
and sorts the generated spec includes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A PlatformContext hybrid object now returns the database, temporary
and legacy database directories. Kotlin reads them from the Android
application context, and Objective-C++ applies the iOS Info.plist
options that OnLoad.mm handled at launch. JavaScript passes them to
the NitroSQLite constructor, so each root resolves databases against
its own directories instead of static paths, and the Android JNI
setter and the iOS +load hook go away.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Encode rows during SQLite reads and preserve native values where JSON cannot represent them.

Co-authored-by: Codex <[email protected]>
REAL columns often store whole numbers. Formatting them as 64-bit integers is
about twice as fast as std::to_chars for doubles and about five times as fast
as SQLite's formatter, which Apple builds use. It also drops the ".0" that
SQLite's formatter appends. Negative zero keeps the double path so it stays -0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apple's libc++ marks floating-point std::to_chars unavailable below iOS 16.3,
so Apple builds formatted every fractional REAL value with SQLite's formatter,
which takes about twice as long. Look the function up at runtime instead and
keep SQLite's formatter only on systems that don't ship it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every result went through JSON.parse, including the empty result of each write
and single-row lookups. That parse has a fixed cost per batch, which made
prepared point lookups 4 to 11% slower than direct JSI conversion on Android
devices. A batch now keeps native values up to 32 cells and switches to JSON
once it grows past that, encoding the rows it has read so far. Streamed batches
also reuse the previous batch's JSON size as their capacity.

The result tests now check each edge case with one row and with 40 rows, so
both conversion paths stay covered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Asynchronous calls copied their parameters to own them on the worker thread,
then copied them again into the queued operation. executeBatchAsync copied
every batch command twice more to replace JS-backed ArrayBuffers. The owned
copies are now moved into the operations, and batch commands replace their
ArrayBuffers in place, so each parameter is copied once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The read benchmarks only measured synchronous queries, where native code
encodes and JavaScript parses results on the same thread. They also stored
only whole numbers in REAL columns, which skip the slower number formatting.
Add asynchronous versions of the narrow and wide reads and a read of 100k rows
with fractional REAL values. The existing cases keep their order and labels so
earlier results stay comparable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Describe each optimization in this branch, the benchmark it targets, and the
stack PR it should move into after benchmarking.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-native-nitro-sqlite Error Error Oct 29, 2026 12:57pm UTC

Request Review

chrispader and others added 2 commits October 29, 2026 12:56
Port the app workload cases from #470: a chat app that stores synced
messages, browses its inbox and conversations, and launches, plus
key-value storage, a JSON response cache, a BLOB thumbnail cache,
async point lookups, and synchronous inserts in one transaction. The
Drizzle and executeRaw cases stay out because this branch has neither
API yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch had an error being deployed

1 failed deployment
Preview — 519e5adf Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant