Skip to content

feat: compress native npm bindings#12000

Open
kdy1 wants to merge 3 commits into
mainfrom
kdy1/compressed-native-bindings
Open

feat: compress native npm bindings#12000
kdy1 wants to merge 3 commits into
mainfrom
kdy1/compressed-native-bindings

Conversation

@kdy1

@kdy1 kdy1 commented Jul 8, 2026

Copy link
Copy Markdown
Member

Description:

Apply compressed native binding loading to the SWC native npm packages. Native .node builds now go through a shared wrapper that first builds the raw napi addon, packs it as zstd pressed-data with SHA-512 integrity, and then builds a small Rust loader as the shipped .node artifact.

The loader verifies and decompresses the embedded payload on first load, then materializes the raw addon through OS transparent compression when available or a per-user cache fallback otherwise. This keeps the public JS require path and exported N-API surface unchanged while reducing native package download/install size.

This also raises all native SWC npm package engines.node entries, including platform packages, to Node 20 or newer, and updates native CI jobs to Node 20.

Validation performed locally:

  • git submodule update --init --recursive
  • cargo fmt --all
  • cargo clippy --all --all-targets -- -D warnings
  • cargo test -p swc_compressed_binding_tool
  • cargo test -p binding_compressed_node_loader
  • cargo test -p swc-releaser
  • cd packages/core && pnpm build:dev && SWC_NATIVE_BINDING_DISABLE_SELF_REPLACE=1 SWC_NATIVE_BINDING_DEBUG=1 pnpm test
  • cd packages/html && pnpm build:dev && SWC_NATIVE_BINDING_DISABLE_SELF_REPLACE=1 SWC_NATIVE_BINDING_DEBUG=1 pnpm test
  • cd packages/html && SWC_NATIVE_BINDING_DISABLE_SELF_REPLACE=1 SWC_NATIVE_BINDING_DEBUG=1 node -e "const html = require('./'); const out = html.minifySync('<div> hi </div>', {}); console.log(out.code);"
  • cd packages/minifier && pnpm build:dev && SWC_NATIVE_BINDING_DISABLE_SELF_REPLACE=1 SWC_NATIVE_BINDING_DEBUG=1 pnpm test
  • cd packages/minifier && SWC_NATIVE_BINDING_CACHE_DIR=$(mktemp -d) SWC_NATIVE_BINDING_DISABLE_SELF_REPLACE=1 SWC_NATIVE_BINDING_DEBUG=1 node -e "require('./').minifySync('function f(){ return 1 }', {})"
  • cd packages/react-compiler && pnpm build:dev && SWC_NATIVE_BINDING_DISABLE_SELF_REPLACE=1 SWC_NATIVE_BINDING_DEBUG=1 pnpm test

BREAKING CHANGE:

Native SWC npm packages now require Node.js 20 or newer.

Related issue (if exists):

N/A

@kdy1 kdy1 requested a review from a team as a code owner July 8, 2026 04:00
kodiakhq[bot]
kodiakhq Bot previously approved these changes Jul 8, 2026
@changeset-bot

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 10f40f6

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@kdy1 kdy1 marked this pull request as draft July 8, 2026 04:01
@socket-security

socket-security Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​getrandom@​0.4.38110093100100
Addedcargo/​decmpfs@​0.1.09610093100100
Addedcargo/​zstd@​0.13.310010093100100

View full report

@socket-security

socket-security Bot commented Jul 8, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo libc is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/assert_cmd@2.1.2cargo/proptest@1.10.0cargo/rand@0.8.6cargo/getrandom@0.4.3cargo/decmpfs@0.1.0cargo/mimalloc@0.1.44cargo/sha2@0.10.9cargo/chrono@0.4.40cargo/dashmap@5.5.3cargo/dashmap@6.1.0cargo/criterion@0.5.1cargo/backtrace@0.3.74cargo/parking_lot@0.12.3cargo/getrandom@0.2.15cargo/sha1@0.10.6cargo/num_cpus@1.16.0cargo/clap@3.2.25cargo/zstd@0.13.3cargo/tikv-jemallocator@0.5.4cargo/reqwest@0.11.27cargo/tempfile@3.12.0cargo/miette@7.6.0cargo/notify@8.2.0cargo/wasmer@6.1.0-rc.3cargo/dialoguer@0.10.4cargo/browserslist-rs@0.19.0cargo/stacker@0.1.20cargo/blake3@1.5.4cargo/codspeed-criterion-compat@3.0.4cargo/wasmer-compiler-cranelift@6.1.0-rc.3cargo/wasmer-wasix@0.601.0-rc.3cargo/tokio@1.48.0cargo/getrandom@0.3.4cargo/wasi-common@38.0.4cargo/wasmtime@38.0.4cargo/libc@0.2.186

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/libc@0.2.186. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@kdy1 kdy1 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is an experiment PR to see the size impact

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d64b0e09b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bindings/binding_compressed_node_loader/src/lib.rs Outdated
Comment thread bindings/binding_compressed_node_loader/src/lib.rs Outdated
Comment thread tools/swc-releaser/src/main.rs Outdated
@codspeed-hq

codspeed-hq Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 189 untouched benchmarks
⏩ 61 skipped benchmarks1


Comparing kdy1/compressed-native-bindings (10f40f6) with main (f36e4b6)

Open in CodSpeed

Footnotes

  1. 61 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.

@kdy1 kdy1 marked this pull request as ready for review July 10, 2026 04:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10f40f601f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
}

write_cache_addon(&cache_path, &raw)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back if the chosen cache path is unwritable

When HOME/XDG_CACHE_HOME/LOCALAPPDATA is set but points at a read-only location, this propagated cache write error makes require('@swc/...') fail instead of trying the temp fallback or another writable path; on Unix this can even fail the first load after try_self_replace has already succeeded because the cache write is still mandatory. This regresses containers/serverless images with non-writable home directories, so the loader should fall back to a writable temp/cache path or load from a temporary file before returning an error.

Useful? React with 👍 / 👎.

],
"engines": {
"node": ">=10"
"node": ">=20"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require a Node release with N-API 9

This engine range still allows Node 20.0–20.2, but the new loader is built with napi-sys's napi9 feature and calls node_api_get_module_file_name, which is gated on N-API 9. Those early Node 20 releases only expose N-API 8, so npm can install this package and then the native loader fails to resolve/execute the symbol at require time; please raise the range to the first Node 20 release with N-API 9 or avoid that API.

Useful? React with 👍 / 👎.

let raw_addon = find_single_node_addon(&raw_dir)?;
let raw = fs::read(&raw_addon)
.with_context(|| format!("failed to read staged raw addon {}", raw_addon.display()))?;
let pressed = pack_pressed_data(&raw, 19)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Strip the raw addon before packing it

In the macOS x64 publish job I checked .github/workflows/publish-npm-package.yml, where strip -x *.node runs after pnpm build; with this line the raw addon has already been read into the compressed payload, so that later strip only trims the small loader and the shipped payload remains unstripped. This regresses the release package/download size for that target, so the raw .node needs to be stripped before pack_pressed_data embeds it.

Useful? React with 👍 / 👎.

)
})?;
}
let outcome = compress_bytes(cache_path, raw, &Gate::any()).map_err(|err| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Write cache addons atomically

When multiple Node processes first load the same binding with an empty shared cache, they all decode and write the deterministic cache_path directly; one process can finish its write and then race with another process truncating/replacing the same .node before load_and_register, producing an intermittent invalid native module load. Please write to a unique temp path and rename into the cache only after the file is complete.

Useful? React with 👍 / 👎.

@kdy1

kdy1 commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

9.2M (9557416 bytes)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant