deno
eb4061f6 - perf: lazy-load more modules in the snapshot (#34061)

Commit
116 days ago
perf: lazy-load more modules in the snapshot (#34061) ## Summary Lazifies a large fraction of the JS code currently baked into the CLI startup snapshot. End result: | | Snapshot blob | Delta | | --- | ---: | ---: | | Before this stack (main) | ~11.4 MB | — | | After | **7,331,556 bytes** | **−~3.1 MB / −27% from main** | | (final commit of stack — web-streams lazification alone) | 9,980,849 → 7,331,556 | −2.65 MB | Verified with `DENO_LOG_LAZY_LOAD=1 deno run hello.js`: **0 lazy loads at startup**, in both TTY and pipe stdout modes. A non-`fetch`/`stream`/`fs.promises`/`node:repl` program no longer pays parse cost for any of those subtrees. ## What's now lazy ### Web platform (final commit) The 208 KB `06_streams.js` polyfill and every ext module that pulls it: - `ReadableStream` / `WritableStream` / `TransformStream` and all their inner controllers/readers (13 stream classes) - `Request` / `Response` / `fetch` / `EventSource` (chain through `22_body.js` → `06_streams.js`) - `caches` / `CacheStorage` / `Cache` - `CompressionStream` / `DecompressionStream` - `node:stream/web` - `Deno.serve` / `Deno.serveHttp` / `Deno.upgradeWebSocket` / `Deno.Command` / `Deno.run` / `Deno.spawn*` / `Deno.kill` / `Deno.openKv` ### Node polyfills (earlier in stack) - HTTP cluster: `node:http` / `node:http2` / `node:https` / `node:_http_*` / `node:internal/http*` - Crypto cluster: `node:crypto` / `node:internal/crypto/{cipher,hash,...}` - Streams cluster: `node:zlib`, `node:repl`, `node:internal/repl`, `node:readline`, `node:readline/promises` - Process cluster: `node:child_process`, `node:internal/child_process`, `node:dgram`, `node:cluster` - TLS cluster: `node:tls`, `node:_tls_common`, `node:_tls_wrap` - Misc: `node:fs/promises`, `node:assert/strict`, `node:internal/event_target`, `node:internal/fs/utils` Kept eager (loading them is on the hot path of every program): `node:stream`, `node:stream/promises`, `node:net`, `node:tty`, `node:module`, `node:process`. ## Overview of changes ### Infrastructure (`build(snapshot)` + `refactor(core)`) - **`DENO_SNAPSHOT_IMPORT_GRAPH=<file>`** env var: dump JSONL of every esm/lazy-script edge during snapshot build. Used to identify exactly which scripts are dragging which polyfills into the snapshot. - **`DENO_LOG_LAZY_LOAD=1`** runtime env var: prints a stderr line each time a lazy_loaded_esm / lazy_loaded_js entry actually parses at runtime. Cache hits suppressed. - **Captured `__bootstrap`** in `01_core.js` so deferred `loadExtScript` calls still find `core`/`primordials`/`internals` after `99_main.js` deletes `globalThis.__bootstrap`. - **Residual `.ts` transpile in `build.rs`**: pre-transpile any `lazy_loaded_js` / `lazy_loaded_esm` file that wasn't consumed at snapshot time so the runtime loader receives parseable JS rather than TypeScript. - **Lazy-ESM resolve fallback**: in `module_map`, if static-import resolve fails, fall back to the lazy ESM source list before erroring (lets `node:_http_*` re-export work without eager registration). ### Bug fixes pulled out of the lazification work - `fix(ext/node)`: defer `lazyLoadProcess()` to `deprecated()` wrapper to break the `assert.ts ↔ process.ts` cycle exposed by lazification. - `fix(core)`: drop the module-map borrow before recursively re-evaluating a lazy ESM module — the prior code held it across `module.evaluate(scope)` and panicked on `RefCell::borrow_mut` during recursive lazy_load_esm. ### Final commit — web-streams chain - `runtime/js/98_global_scope_shared.js`: converts every streams-pulling global to `propNonEnumerableLazyLoaded` / wrapper-function form. - `runtime/js/99_main.js`: stops spreading `denoNs` with `{...denoNs}` (which invokes every getter); uses `ObjectDefineProperties + getOwnPropertyDescriptors` instead. Same fix for the unstable-feature merge loop. Wraps the wasm-streaming callback and defers `registerDeclarativeServer` to the `addMainModuleHandler` callback. - `ext/web/13_message_port.js`: drops top-level streams import; `markNotSerializable` registration moved into `06_streams.js` itself (inverts the dep so message_port no longer drags streams). - `ext/node/polyfills/01_require.js`: lazifies `internal/child_process` (which pulled `40_process.js → 22_body.js`) and `stream/web` (which pulled `14_compression.js`). - `ext/node/polyfills/internal/streams/fast-utf8-stream.js`: replaces `import * as fs from "node:fs"` with `createLazyLoader("node:fs")`. The static import was re-entering `node:fs`'s evaluating body and TDZ-trapping on `lazyUtf8Stream().default`. - `ext/node/polyfills/internal/fs/{handle,promises}.ts`: defers every top-level `promisify(lazyFs().X)` to first-call wrappers. Same TDZ cycle: `node:fs`'s `export const promises = mod.promises` line re-triggers `get promises` while `lazyInternalPromises().default` is still in TDZ. ## Outcome | Surface | Improvement | | --- | --- | | Snapshot size | 11.4 MB → **7.33 MB** (−3.1 MB / −27%) | | `deno run empty.js` startup parses | 0 lazy loads in TTY and pipe modes | | `import 'node:crypto'` cost | Paid by users of crypto (3 lazy loads) | | `import 'node:http'` cost | Paid by users of http (9 lazy loads) | | `fetch('...')` first-call cost | Loads `26_fetch.js` + `22_body.js` + `06_streams.js` on demand | Programs that don't touch streams/fetch/http/repl/Deno.serve no longer pay the parse cost. ## Test plan - [ ] `cargo test` passes - [ ] `cargo test --test node_compat` passes (down from 43 → ~38 fails, the remainder are pre-existing on main: `IO Safety violation` in `fork` and the v8 weak-handle GC flake in `test-repl-tab-complete-buffer`) - [ ] `DENO_LOG_LAZY_LOAD=1 deno run empty.js` prints 0 lazy loads (TTY and pipe) - [ ] Smokes: `Deno.serve`, `fetch`, `new ReadableStream/Request/Response`, `structuredClone(new ReadableStream())` rejection, `fs.promises.readdir/readFile`, `node:child_process.spawn`, `node:stream/web` --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Author
Parents
Loading