Skip to content

feat(node_binding): compressed @rspack/binding via a self-loading hybrid .node (RFC/draft)#14719

Open
jdalton wants to merge 1 commit into
web-infra-dev:mainfrom
jdalton:feat/compressed-binding
Open

feat(node_binding): compressed @rspack/binding via a self-loading hybrid .node (RFC/draft)#14719
jdalton wants to merge 1 commit into
web-infra-dev:mainfrom
jdalton:feat/compressed-binding

Conversation

@jdalton

@jdalton jdalton commented Jul 7, 2026

Copy link
Copy Markdown

Draft / RFC — checking the direction before finishing the native part.

Today rspack ships @rspack/binding (the .node file) uncompressed. This ships the real .node zstd-compressed inside the file and, on first load, hands it to the OS's own transparent filesystem compression (APFS / NTFS / btrfs). The kernel then decompresses it on read, so it takes fewer blocks on disk and loads faster — reading fewer bytes beats the cheap kernel decompress. It needs no change to @napi-rs/cli — the loader is rspack's own Rust, reusing the decmpfs crate.

Compressed @rspack/binding: 56% smaller on disk and 28% faster to load

  • The shipped .node download is 13.8 MiB (zstd-19, −64%) instead of 38.4 MiB.
  • On APFS it takes 17.0 MiB on disk (−56%), and the bytes programs see are byte-identical.
  • It loads faster, not slower: cold first load (fresh clone, new process) 853 ms → 615 ms (−28%) on darwin-arm64, because the kernel reads far fewer bytes. Every load after is the same. (Same addon, same measurement as the aube benchmark linked below.)

Related: the napi-rs --compress proposal, and the same idea shipped in aube — store compression and its benchmark.

How it works

The real .node is stored zstd-compressed in a dedicated section of the shipped file, with a SHA-512 hash so a corrupted download is caught. On the first load, rspack reads that section, checks the hash, unzips it, and then rewrites the file compressed in place with the OS's transparent filesystem compression (APFS / NTFS / btrfs) so the kernel handles it from then on; on a filesystem that can't (ext4, most NFS) it falls back to unzipping once into a small per-version cache. Then it hands off to the normal addon. This is the same section layout the decmpfs crate already reads, so that unwrap code is reused.

What's in this PR
  • crates/node_binding/scripts/pack-compressed-binding.mjs — a reference packer that builds the compressed payload (zstd + SHA-512) in the exact layout decmpfs reads. Verified: it packs a real .node and unpacks to byte-identical bytes that load cleanly.
  • crates/node_binding/scripts/pack-compressed-binding.test.mjsnode:test unit tests for the packer: pack/unpack round-trip, magic + header shape, and rejection of a short buffer, wrong magic, tampered payload (SHA-512 mismatch), and an implausible size. Run: node --test crates/node_binding/scripts/pack-compressed-binding.test.mjs.
  • crates/node_binding/scripts/bench-compressed-binding.mjs — resolves whichever @rspack/binding is installed for the current platform (no hardcoded version or path) and reports the size numbers above.
  • crates/node_binding/docs/compressed-binding.md — a short design doc.
What's still to do (on rspack's CI)
  • Inject the compressed section into the built .node for each platform.
  • The Rust code that unpacks, rewrites filesystem-compressed, and hands off on the first load.

Both need rspack's cross-platform build to test, which is why this is a draft.

We plan to upstream this to @napi-rs/cli eventually. Once the CLI can compress the addon itself, this shrinks to a one-line build flag and the extra code here is no longer needed. Until then it keeps the win in rspack's tree.

@jdalton jdalton marked this pull request as ready for review July 7, 2026 18:32
Copilot AI review requested due to automatic review settings July 7, 2026 18:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This draft/RFC introduces a proposed “hybrid self-loading” compressed format for @rspack/binding (shipping a .node that contains a zstd-compressed payload and integrity hash), along with reference tooling and documentation to validate the section layout and measure size impact.

Changes:

  • Add a reference pack/unpack implementation for the PRESSED_DATA section payload layout (zstd + SHA-512).
  • Add a local benchmark script that locates the installed platform binding and reports raw vs zstd-19 size (and compression time).
  • Add a short design doc describing the hybrid format and intended runtime behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
crates/node_binding/scripts/pack-compressed-binding.mjs Reference pack/unpack logic for the pressed-data payload layout (zstd + SHA-512).
crates/node_binding/scripts/bench-compressed-binding.mjs Local benchmark utility to resolve installed binding and report size/compression metrics.
crates/node_binding/docs/compressed-binding.md Design/RFC documentation for the hybrid compressed binding approach and runtime plan.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/node_binding/scripts/pack-compressed-binding.mjs Outdated
Comment thread crates/node_binding/scripts/pack-compressed-binding.mjs Outdated
Comment thread crates/node_binding/scripts/pack-compressed-binding.mjs
Comment thread crates/node_binding/scripts/bench-compressed-binding.mjs Outdated
…rid .node

Ship the native addon zstd-compressed inside the .node and unwrap it on first
load, handing it to the OS's transparent filesystem compression (APFS / NTFS /
btrfs) so it takes fewer blocks on disk and loads faster (the kernel reads
fewer bytes), reusing the decmpfs crate — no @napi-rs/cli change.

- scripts/pack-compressed-binding.mjs: reference packer (zstd + SHA-512) in the
  layout decmpfs unwraps; unpack() fails closed on a short, tampered, oversized,
  or non-zstd section.
- scripts/pack-compressed-binding.test.mjs: node:test unit tests (round-trip,
  integrity, rejection paths), pure in-memory.
- scripts/bench-compressed-binding.mjs: resolves whichever @rspack/binding is
  installed for the platform and reports the download size win.
- docs/compressed-binding.md: design doc.

Draft/RFC — per-platform section injection and the Rust first-load loader still
need rspack's cross-compile CI.
@codspeed-hq

codspeed-hq Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing jdalton:feat/compressed-binding (d6608c6) with main (f489568)

Open in CodSpeed

Footnotes

  1. 47 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

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.

2 participants