Skip to content

sea: add vfsArchive to serve the assets from a ZIP archive - #65810

Draft
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:sea-vfs-zip
Draft

sea: add vfsArchive to serve the assets from a ZIP archive#65810
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:sea-vfs-zip

Conversation

@mcollina

@mcollina mcollina commented Sep 5, 2026

Copy link
Copy Markdown
Member

This adds a "vfsArchive" option to the SEA configuration that serves the bundled assets from a ZIP archive, and fixes a performance bug in the SEA asset lookup found while benchmarking it.

"vfsArchive"

Instead of listing individual "assets", the configuration can point at a prebuilt ZIP archive:

{
  "main": "main.js",
  "output": "app",
  "useVfs": true,
  "vfsArchive": "assets.zip"
}

--build-sea embeds the archive verbatim as a single reserved asset (only a PK signature sanity check happens at build time β€” no ZIP serialization logic is added to the build side). At runtime the SEA virtual file system mounts the existing ZipProvider over a zero-copy view of the embedded archive instead of the SEAProvider, and the main script is injected into the in-memory archive index as a stored entry, so the mounted tree looks exactly like plain "useVfs": __dirname-relative reads, relative require(), and bare specifier lookups are unchanged.

The archive can be produced with any ZIP tool or with the ZIP support in node:zlib (zlib.zipFiles()), which is also what the test does. "vfsArchive" requires "useVfs": true and cannot be combined with "assets". Since only the archive is embedded, sea.getAsset()/sea.getAssetAsBlob() do not serve the individual files; they are read through the fs APIs instead.

Perf fix: return the SEA resource by reference

FindSingleExecutableResource() returned the deserialized SeaResource by value, copying the whole assets map on every call. getAsset() calls it once per asset read, making every fs operation served by the SEA VFS pay a cost linear in the number of bundled assets: with 8192 assets, reading each of them took ~7.9s instead of ~270ms (~30x). Fixed in the first commit by returning a reference to the cached resource.

Size / speed tradeoff

Measured with a 75% text / 25% random asset mix in 16KB files (Linux x64, warm cache, medians of 15 runs; scenarios: start the binary touching no asset, read one small asset, read every asset):

Assets Binary (plain β†’ archive) Startup +Ξ” Read one +Ξ” Read all (plain β†’ archive)
1MB 151.3MB β†’ 150.6MB +12ms +14ms 50ms β†’ 64ms
8MB 158.7MB β†’ 153.0MB +11ms +12ms 69ms β†’ 110ms
32MB 183.9MB β†’ 161.2MB (βˆ’22.7MB) +12ms +19ms 115ms β†’ 213ms
128MB 284.7MB β†’ 193.9MB (βˆ’90.8MB, βˆ’32%) +8ms +11ms 269ms β†’ 477ms
  • The startup penalty is a flat ~10ms independent of bundle size (loading the internal/zip machinery and parsing the central directory); nothing is inflated until a file is opened.
  • Reads from the archive are ~2.5x slower than the SEAProvider memcpy path; per-file stat cost is identical (VFS dispatch dominates).
  • Max RSS is lower with the archive for workloads that read a subset of the assets (119MB vs 198MB at the 128MB tier) because fewer executable pages are touched.

Rule of thumb: the archive is a clear win above roughly 30MB of compressible assets when a run reads a subset of them; plain "useVfs" remains better for small bundles or workloads that repeatedly read large assets.

FindSingleExecutableResource() returned the deserialized SeaResource by
value, copying the whole assets map on every call. getAsset() calls it
once per asset read, which made every fs operation served by the SEA
virtual file system pay a cost linear in the number of bundled assets:
with 8192 assets, reading each of them took seconds instead of
milliseconds. Return a reference to the cached resource instead.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
When "vfsArchive" names a ZIP archive in the SEA configuration (with
"useVfs": true), --build-sea embeds the archive verbatim as one
reserved asset, and at runtime the SEA virtual file system mounts the
existing ZipProvider over a zero-copy view of the embedded archive
instead of the SEAProvider. The main script is injected into the
in-memory archive index as a stored entry, so the mounted tree looks
the same as with "assets".

The archive can be produced with any ZIP tool or with the ZIP support
in node:zlib; no ZIP serialization logic is added to the build side.
This trades asset read speed (entries are inflated when opened) for a
substantially smaller executable when the assets are compressible.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/single-executable
  • @nodejs/startup
  • @nodejs/vm

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants