Skip to content

perf: reduce build memory and time - #1156

Open
ovflowd wants to merge 14 commits into
mainfrom
perf/memory-usage
Open

ovflowd wants to merge 14 commits into
mainfrom
perf/memory-usage

Conversation

@ovflowd

@ovflowd ovflowd commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Description

Building the Node.js docs peaks at 3.6–3.8GB of memory, while a single page only needs ~100MB. Node core only builds the HTML docs on machines with more than 5GiB of memory, and silently skips them otherwise. That's what happens on its ubuntu-slim doc CI, whose docs artifact has no HTML at all.

This PR brings the peak down to 1.7–2.1GB, and the build time to about a third:

  • Shiki registers grammars on demand. Once a thread highlighted code, the Shiki plugin registered all ~250 grammars Shiki bundles, and importing it imported all of them on every thread loading jsx-ast. The docs use about ten languages. Each one is now registered the first time code in it is highlighted, with the languages it embeds, which takes highlighting from ~17s of CPU to ~4s, and the themes go to Shiki by name, so it stops parsing them again for every block. Code highlights the same whatever a thread highlighted before: fences in a Markdown block are registered with it, and a grammar injecting into others (ts-tags) only does so in its own blocks. @node-core/rehype-shiki is updated to 1.5.1, whose plugin only imports every grammar when it isn't given a highlighter (nodejs/nodejs.org#9212).
  • Highlighted code reaches pages as static markup. Every highlighted token became a hast node, then a JSX element, then generated code, only to be rendered back into the same HTML. A plugin after Shiki writes the code blocks out as HTML with hast-util-to-html, and types and signatures are embedded as they're highlighted, before rehype-raw would parse each of their tokens again. Each thread also highlights a type only once (of the ~20,000 types the docs highlight, 741 differ).
  • all.html renders in the worker pool. It was rendered and minified on the main thread, after every other page, holding the whole site there (~1GB).
  • HTML isn't minified anymore. Every page went through SWC's HTML minifier, compiled to WASM. Its memory grows to ~12× the biggest page it's given (~400MB for the 33MB all.html) and is never returned, so every thread rendering pages kept it until it ended, and minifying took a fifth of the build's CPU. On Node 24, the worker holding that memory collected garbage on nearly every allocation, which took the build from ~8s to ~52s. In exchange, the 741 pages were 5.9% smaller on disk, but only 3.2% smaller gzipped and 2.1% with brotli, which is how they're served: Cloudflare, Vercel, Netlify and GitHub Pages compress HTML on their own, and so do doc-kit.nodejs.org and nodejs.org. The pages, legacy-html's included, are now written as rendered, @swc/html-wasm is gone, and docs/publishing.md shows how to serve them compressed. minify still minifies scripts, styles and JSON.
  • Idle workers end after 250ms instead of a second, so the jsx-ast workers' heaps (~800MB) are gone while the main thread bundles the site, which is when the build peaked.
  • Worker heaps are limited to 512MB. V8 lets a heap grow to several times its live data before collecting it, four times with the 4GB limit workers get on machines with 16GB+. It costs 3–5% of build time, spent collecting garbage. Node's --max-old-space-size replaces the limit, to raise it.
  • Smaller fixes: the bundler is let go before the pages render (~200MB with one thread), and each thread only imports the modules it uses. That includes the logger, whose GitHub Actions transport now writes its workflow commands itself, for the same output, rather than importing @actions/core and undici on every thread (237 modules, 19MB and ~80ms per thread, down to 10 modules, 3MB and 7ms).
  • The legacy HTML generator's Shiki plugin is marked as deprecated, as it will be removed soon.

How a build runs

The main thread schedules the generators, holds their results and bundles the site with Vite. The rest of the work goes to a pool of worker threads, which end once idle:

sequenceDiagram
  participant M as Main thread
  participant W as Worker pool (Piscina)

  Note over W: up to 4 workers, 512MB of heap each
  M->>W: ast, metadata and jsx-ast chunks
  W-->>M: each page's code, highlighted code as static markup
  Note over W: workers idle for 250ms end
  Note over M: Vite bundles the component library and the client assets, then compiles each page program
  M->>W: page tasks, all.html first
  Note over W: import, render, template, write
  W-->>M: written pages
Loading

With threads: 1, the main thread runs the chunks itself.

Validation

  • node --run test (747 tests) passes at every commit, and node --run format:check and node --run lint pass.
  • Every page matches main once both are minified with main's minifier and parsed as HTML (hast-util-to-html spells a few characters in code differently than Preact did, such as < for <), on the web target (71 pages), on Node core's config (741) and on legacy-html (69), except the C++ code blocks of addons, embedding and n-api (and all.html, which holds them), whose grammar now comes from core's own shiki (4.4.3) instead of the one @node-core/rehype-shiki 1.5.0 pinned (4.3.1).
  • With Node's --max-old-space-size, which limits every isolate, the Node core build passes at 384MB (not at 320MB), where main needs 768MB.

Benchmarks

main → this branch, building Node core's docs with its config (legacy-json-all + section-pages) unless noted. Medians of 3 interleaved runs on an M4 Pro; peak memory is the physical footprint of every process in the build.

Build Peak memory Duration CPU time
Node 26, 4 threads 3,626MB → 1,654MB (-54%) 22.5s → 7.7s 83.7s → 32.5s
Node 26, 1 thread 3,787MB → 2,017MB (-47%) 49.3s → 15.0s 60.7s → 20.3s
Node 24, 4 threads 3,823MB → 2,098MB (-45%) 23.8s → 7.7s 90.7s → 34.1s
web target, Node 26 3,335MB → 1,598MB (-52%) 17.8s → 5.8s 58.0s → 23.7s

With one thread, the main thread does all the work, and the workers' heap limit doesn't apply. Node's --max-old-space-size=768 brings that build to 1.47GB in about the same time.

Related Issues

Refs: #815
Refs: nodejs/node#62045

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run node --run test and all tests passed.
  • I have check code formatting with node --run format:check & node --run lint.
  • I've covered new added functionality with unit tests if necessary.

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
api-docs-tooling Ready Ready Preview Oct 12, 2026 3:55pm UTC

Request Review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ d4a1b61 2026-10-12T15:26:32.799Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ df529ef 2026-10-12T15:14:20.329Z View logs ↗
  • Build: Failed ❌

View logs ↗
f724336 2026-10-12T02:07:31.759Z View logs ↗
  • Build: Failed ❌

View logs ↗
7c7dfd7 2026-10-12T00:57:48.518Z View logs ↗
  • Build: Failed ❌

View logs ↗
b1b2556 2026-10-11T22:48:28.395Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ fa3c3e4 2026-10-11T22:29:17.395Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ d8a7c0d 2026-10-11T22:01:16.275Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ 5d71718 2026-10-11T19:46:36.480Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ 082b53e 2026-10-11T19:03:21.758Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ d80d5fe 2026-10-11T18:54:19.318Z View logs ↗

View all previews: View all previews ↗

@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.90572% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.49%. Comparing base (7238bb1) to head (d4a1b61).

Files with missing lines Patch % Lines
packages/core/src/plugins/shiki/highlighter.mjs 95.10% 9 Missing ⚠️
packages/core/src/logger/transports/github.mjs 97.91% 1 Missing ⚠️
...kages/node-legacy/src/legacy-html-all/generate.mjs 0.00% 1 Missing ⚠️
packages/node-legacy/src/legacy-html/generate.mjs 0.00% 1 Missing ⚠️
...ages/node-legacy/src/legacy-html/plugins/shiki.mjs 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1156      +/-   ##
==========================================
+ Coverage   93.32%   93.49%   +0.17%     
==========================================
  Files         273      278       +5     
  Lines       27050    27790     +740     
  Branches     2722     2788      +66     
==========================================
+ Hits        25244    25983     +739     
- Misses       1784     1785       +1     
  Partials       22       22              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

api-links Generator

Output: 1 file differs

apilinks.json
Expected values to be strictly deep-equal:
+ actual - expected
... Skipped lines

  {
    'Agent.defaultMaxSockets': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L300',
    'Buffer.alloc': 'https://github.com/{repository}/blob/HEAD/lib/buffer.js#L454',
    'Buffer.allocUnsafe': 'https://github.com/{repository}/blob/HEAD/lib/buffer.js#L473',
    'Buffer.allocUnsafeSlow': 'https://github.com/{repository}/blob/HEAD/lib/buffer.js#L496',
...
    'agent.addRequest': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L365',
+   'agent.createConnection': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L304',
-   'agent.createConnection': 'https://github.com/{repository}/blob/HEAD/lib/https.js#L354',
    'agent.createSocket': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L446',
    'agent.destroy': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L679',
+   'agent.getName': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L334',
+   'agent.keepSocketAlive': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L635',
-   'agent.getName': 'https://github.com/{repository}/blob/HEAD/lib/https.js#L537',
-   'agent.keepSocketAlive': 'https://github.com/{repository}/blob/HEAD/lib/https.js#L506',
    'agent.removeSocket': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L574',
    'agent.reuseSocket': 'https://github.com/{repository}/blob/HEAD/lib/_http_agent.js#L671',
    'assert.assert': 'https://github.com/{repository}/blob/HEAD/lib/assert.js#L185',
    'asyncResource.asyncId': 'https://github.com/{repository}/blob/HEAD/lib/async_hooks.js#L243',
    'asyncResource.bind': 'https://github.com/{repository}/blob/HEAD/lib/async_hooks.js#L275',
...
    'server.address': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L2885',
+   'server.close': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L697',
+   'server.closeAllConnections': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L707',
+   'server.closeIdleConnections': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L719',
-   'server.close': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L3018',
-   'server.closeAllConnections': 'https://github.com/{repository}/blob/HEAD/lib/https.js#L125',
-   'server.closeIdleConnections': 'https://github.com/{repository}/blob/HEAD/lib/https.js#L127',
    'server.getConnections': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L2980',
    'server.listen': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L2675',
    'server.ref': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L3132',
+   'server.setTimeout': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L735',
-   'server.setTimeout': 'https://github.com/{repository}/blob/HEAD/lib/https.js#L129',
    'server.unref': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L3141',
+   'server[SymbolAsyncDispose]': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L703',
-   'server[SymbolAsyncDispose]': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L3059',
    'server[SymbolAsyncIterator]': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L3066',
    'server[kDeserialize]': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L2596',
    'server[kTransferList]': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L2568',
    'server[kTransfer]': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L2573',
+   'server[undefined]': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L742',
-   'server[undefined]': 'https://github.com/{repository}/blob/HEAD/lib/net.js#L3096',
    'serverresponse._finish': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L262',
    'serverresponse._implicitHeader': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L419',
    'serverresponse.assignSocket': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L312',
    'serverresponse.detachSocket': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L323',
    'serverresponse.statusCode': 'https://github.com/{repository}/blob/HEAD/lib/_http_server.js#L285',

Performance estimate (single CI run)

  • Generation time: 20.0% faster (1.40 s → 1.12 s)
  • Peak memory: 25.8% lower (434.64 MB → 322.66 MB)

json Generator

Performance estimate (single CI run)

  • Generation time: 16.8% faster (9.01 s → 7.50 s)
  • Peak memory: 18.1% lower (1.49 GB → 1.22 GB)

legacy-html Generator

Output size: 70 files changed · net +836.50 KB

File size details
File Main PR Change
fs.html 1.10 MB 1.15 MB +46.21 KB (+4.1%)
buffer.html 1.13 MB 1.17 MB +41.99 KB (+3.6%)
crypto.html 1.15 MB 1.19 MB +41.98 KB (+3.6%)
test.html 958.38 KB 989.95 KB +31.57 KB (+3.3%)
stream.html 847.91 KB 877.80 KB +29.89 KB (+3.5%)
http2.html 787.78 KB 814.87 KB +27.08 KB (+3.4%)
util.html 777.15 KB 803.62 KB +26.47 KB (+3.4%)
process.html 706.34 KB 731.24 KB +24.90 KB (+3.5%)
deprecations.html 284.57 KB 309.25 KB +24.68 KB (+8.7%)
http.html 687.26 KB 711.86 KB +24.59 KB (+3.6%)
n-api.html 798.97 KB 822.51 KB +23.54 KB (+2.9%)
addons.html 354.63 KB 374.15 KB +19.52 KB (+5.5%)
events.html 556.58 KB 575.98 KB +19.39 KB (+3.5%)
perf_hooks.html 520.60 KB 539.47 KB +18.88 KB (+3.6%)
assert.html 473.27 KB 491.16 KB +17.88 KB (+3.8%)
zlib.html 481.83 KB 499.44 KB +17.61 KB (+3.7%)
stream_iter.html 499.24 KB 516.19 KB +16.95 KB (+3.4%)
errors.html 406.81 KB 423.20 KB +16.39 KB (+4.0%)
child_process.html 448.83 KB 464.73 KB +15.90 KB (+3.5%)
cli.html 354.61 KB 369.87 KB +15.25 KB (+4.3%)
vm.html 431.80 KB 446.63 KB +14.83 KB (+3.4%)
webcrypto.html 356.32 KB 370.68 KB +14.36 KB (+4.0%)
worker_threads.html 403.41 KB 417.72 KB +14.31 KB (+3.5%)
quic.html 443.81 KB 457.53 KB +13.72 KB (+3.1%)
module.html 349.03 KB 361.77 KB +12.73 KB (+3.6%)
url.html 332.58 KB 345.15 KB +12.57 KB (+3.8%)
v8.html 341.02 KB 353.32 KB +12.30 KB (+3.6%)
diagnostics_channel.html 334.45 KB 346.27 KB +11.82 KB (+3.5%)
webstreams.html 285.83 KB 296.93 KB +11.11 KB (+3.9%)
net.html 292.15 KB 302.83 KB +10.68 KB (+3.7%)
tls.html 286.40 KB 296.75 KB +10.35 KB (+3.6%)
sqlite.html 290.77 KB 301.08 KB +10.32 KB (+3.5%)
dns.html 225.99 KB 234.87 KB +8.88 KB (+3.9%)
async_context.html 238.23 KB 247.09 KB +8.86 KB (+3.7%)
repl.html 218.11 KB 226.56 KB +8.45 KB (+3.9%)
readline.html 210.82 KB 219.03 KB +8.21 KB (+3.9%)
report.html 189.67 KB 197.77 KB +8.10 KB (+4.3%)
cluster.html 200.84 KB 208.83 KB +7.99 KB (+4.0%)
async_hooks.html 224.04 KB 231.99 KB +7.95 KB (+3.5%)
globals.html 144.98 KB 152.65 KB +7.67 KB (+5.3%)
https.html 158.69 KB 165.49 KB +6.79 KB (+4.3%)
dgram.html 171.77 KB 178.44 KB +6.67 KB (+3.9%)
dtls.html 177.35 KB 183.72 KB +6.37 KB (+3.6%)
packages.html 152.72 KB 159.01 KB +6.28 KB (+4.1%)
modules.html 157.69 KB 163.71 KB +6.02 KB (+3.8%)
esm.html 133.83 KB 139.25 KB +5.42 KB (+4.0%)
vfs.html 138.90 KB 144.00 KB +5.11 KB (+3.7%)
inspector.html 119.39 KB 124.33 KB +4.94 KB (+4.1%)
debugger.html 110.69 KB 115.47 KB +4.78 KB (+4.3%)
path.html 99.43 KB 104.08 KB +4.65 KB (+4.7%)
embedding.html 59.79 KB 64.29 KB +4.51 KB (+7.5%)
console.html 99.05 KB 103.50 KB +4.45 KB (+4.5%)
single-executable-applications.html 119.07 KB 123.50 KB +4.43 KB (+3.7%)
domain.html 102.82 KB 107.22 KB +4.39 KB (+4.3%)
timers.html 92.24 KB 96.52 KB +4.28 KB (+4.6%)
ffi.html 109.20 KB 113.38 KB +4.18 KB (+3.8%)
bench.html 110.26 KB 114.27 KB +4.01 KB (+3.6%)
tracing.html 90.71 KB 94.57 KB +3.86 KB (+4.3%)
os.html 88.99 KB 92.56 KB +3.57 KB (+4.0%)
permissions.html 70.87 KB 73.81 KB +2.94 KB (+4.1%)
documentation.html 26.97 KB 29.75 KB +2.78 KB (+10.3%)
tty.html 47.62 KB 50.39 KB +2.77 KB (+5.8%)
wasi.html 46.18 KB 48.76 KB +2.57 KB (+5.6%)
string_decoder.html 40.88 KB 43.28 KB +2.39 KB (+5.9%)
querystring.html 34.84 KB 37.11 KB +2.27 KB (+6.5%)
intl.html 43.96 KB 46.20 KB +2.24 KB (+5.1%)
typescript.html 35.11 KB 37.34 KB +2.23 KB (+6.3%)
punycode.html 35.55 KB 37.72 KB +2.17 KB (+6.1%)
environment_variables.html 26.11 KB 27.94 KB +1.83 KB (+7.0%)
index.html 17.76 KB 19.45 KB +1.69 KB (+9.5%)

Performance estimate (single CI run)

  • Generation time: 48.2% faster (39.43 s → 20.43 s)
  • Peak memory: 25.2% lower (2.48 GB → 1.85 GB)

legacy-json Generator

Performance estimate (single CI run)

  • Generation time: 6.1% slower (8.36 s → 8.87 s)
  • Peak memory: 28.0% lower (1.57 GB → 1.13 GB)

llms-txt Generator

Performance estimate (single CI run)

  • Generation time: 92.8% slower (4.87 s → 9.39 s)
  • Peak memory: 34.7% lower (1.77 GB → 1.16 GB)

orama-db Generator

Output size: 1 file changed · net -121.00 B

File size details
File Main PR Change
orama-db.json 9.57 MB 9.57 MB -121.00 B (-0.0%)

Performance estimate (single CI run)

  • Generation time: 59.4% slower (5.52 s → 8.80 s)
  • Peak memory: 35.7% lower (1.84 GB → 1.18 GB)

web Generator

Output size: 136 files changed · net +3.34 MB

File size details
File Main PR Change
all.html 33.10 MB 34.69 MB +1.59 MB (+4.8%)
fs.html 2.21 MB 2.32 MB +108.52 KB (+4.8%)
crypto.html 1.96 MB 2.04 MB +86.04 KB (+4.3%)
assets/SearchBox-wQQHMZuJ.js 84.67 KB — -84.67 KB (-100.0%)
assets/SearchBox-DjNnuz7W.js — 84.67 KB +84.67 KB
buffer.html 1.75 MB 1.82 MB +79.62 KB (+4.5%)
quic.html 925.00 KB 988.17 KB +63.18 KB (+6.8%)
http2.html 1.25 MB 1.31 MB +62.49 KB (+4.9%)
http.html 1.17 MB 1.23 MB +61.02 KB (+5.1%)
process.html 1.14 MB 1.20 MB +57.43 KB (+4.9%)
test.html 1.38 MB 1.44 MB +57.42 KB (+4.1%)
util.html 1.21 MB 1.26 MB +57.26 KB (+4.6%)
n-api.html 1.00 MB 1.05 MB +54.86 KB (+5.4%)
stream.html 1.22 MB 1.27 MB +54.63 KB (+4.4%)
zlib.html 948.66 KB 998.52 KB +49.86 KB (+5.3%)
cli.html 628.19 KB 675.82 KB +47.62 KB (+7.6%)
perf_hooks.html 865.52 KB 911.96 KB +46.44 KB (+5.4%)
deprecations.html 564.51 KB 608.45 KB +43.94 KB (+7.8%)
webcrypto.html 641.13 KB 681.64 KB +40.51 KB (+6.3%)
errors.html 531.73 KB 570.79 KB +39.05 KB (+7.3%)
events.html 831.58 KB 869.10 KB +37.52 KB (+4.5%)
net.html 633.53 KB 670.90 KB +37.38 KB (+5.9%)
stream_iter.html 756.55 KB 789.18 KB +32.64 KB (+4.3%)
webstreams.html 534.61 KB 566.50 KB +31.88 KB (+6.0%)
worker_threads.html 632.52 KB 663.11 KB +30.59 KB (+4.8%)
assets/dist-D5dOZyAD.js 30.42 KB — -30.42 KB (-100.0%)
assets/dist-DruvUqXN.js — 30.42 KB +30.42 KB
child_process.html 672.20 KB 699.32 KB +27.12 KB (+4.0%)
tls.html 522.81 KB 549.69 KB +26.88 KB (+5.1%)
vm.html 631.19 KB 657.44 KB +26.25 KB (+4.2%)
v8.html 549.36 KB 575.25 KB +25.88 KB (+4.7%)
assert.html 647.96 KB 673.69 KB +25.73 KB (+4.0%)
assets/SideBar-CjBhW9Hu.js 25.62 KB — -25.62 KB (-100.0%)
assets/SideBar-DaKBoTPQ.js — 25.62 KB +25.62 KB
diagnostics_channel.html 527.43 KB 552.72 KB +25.29 KB (+4.8%)
sqlite.html 518.73 KB 543.71 KB +24.98 KB (+4.8%)
assets/client-eAdjoyYV.js 24.91 KB — -24.91 KB (-100.0%)
assets/client-DxC3O6Ca.js — 24.91 KB +24.91 KB
assets/config-Cx-IX3nR.js 24.37 KB — -24.37 KB (-100.0%)
assets/config-DtT2jlD0.js — 24.09 KB +24.09 KB
url.html 510.96 KB 534.56 KB +23.60 KB (+4.6%)
module.html 508.39 KB 530.78 KB +22.39 KB (+4.4%)
dns.html 420.60 KB 442.76 KB +22.16 KB (+5.3%)
globals.html 281.24 KB 303.02 KB +21.78 KB (+7.7%)
readline.html 362.24 KB 381.45 KB +19.21 KB (+5.3%)
dtls.html 299.32 KB 316.25 KB +16.93 KB (+5.7%)
assets/Combination-0Ghc6FKv.js 16.11 KB — -16.11 KB (-100.0%)
assets/Combination-d5oRXla-.js — 16.11 KB +16.11 KB
dgram.html 298.44 KB 314.05 KB +15.62 KB (+5.2%)
cluster.html 302.76 KB 318.27 KB +15.51 KB (+5.1%)
async_context.html 316.64 KB 330.35 KB +13.71 KB (+4.3%)
assets/ThemeToggle-BZ0_KYDS.js 13.65 KB — -13.65 KB (-100.0%)
assets/ThemeToggle-DVZdvZ7l.js — 13.65 KB +13.65 KB
repl.html 301.06 KB 314.32 KB +13.26 KB (+4.4%)
packages.html 217.14 KB 229.71 KB +12.57 KB (+5.8%)
inspector.html 213.23 KB 225.65 KB +12.42 KB (+5.8%)
modules.html 226.09 KB 238.40 KB +12.31 KB (+5.4%)
addons.html 377.82 KB 389.90 KB +12.08 KB (+3.2%)
async_hooks.html 281.56 KB 293.41 KB +11.84 KB (+4.2%)
https.html 239.14 KB 250.52 KB +11.38 KB (+4.8%)
timers.html 181.40 KB 192.67 KB +11.27 KB (+6.2%)
vfs.html 202.81 KB 213.81 KB +11.01 KB (+5.4%)
bench.html 202.84 KB 213.82 KB +10.98 KB (+5.4%)
console.html 183.92 KB 194.58 KB +10.66 KB (+5.8%)
path.html 175.98 KB 186.51 KB +10.53 KB (+6.0%)
ffi.html 193.59 KB 204.00 KB +10.41 KB (+5.4%)
esm.html 186.08 KB 196.41 KB +10.33 KB (+5.5%)
assets/dist-5O-XzCME.js 10.20 KB — -10.20 KB (-100.0%)
assets/dist-DVEbYasF.js — 10.20 KB +10.20 KB
assets/Layout-N2aBwWVg.js 10.13 KB — -10.13 KB (-100.0%)
assets/Layout-C5yPmBoc.js — 10.13 KB +10.13 KB
assets/compat-DmJKK7tl.js 10.13 KB — -10.13 KB (-100.0%)
assets/compat-ByVaDh64.js — 10.13 KB +10.13 KB
os.html 163.73 KB 173.79 KB +10.07 KB (+6.1%)
report.html 230.15 KB 238.26 KB +8.10 KB (+3.5%)
single-executable-applications.html 158.11 KB 166.12 KB +8.00 KB (+5.1%)
tty.html 106.11 KB 114.10 KB +7.98 KB (+7.5%)
assets/Tooltip-_V0pbK8z.js 7.93 KB — -7.93 KB (-100.0%)
assets/Tooltip-C41LEB8U.js — 7.93 KB +7.93 KB
assets/dist-CX7YxNak.js 6.99 KB — -6.99 KB (-100.0%)
assets/dist-DNbUuC3x.js — 6.99 KB +6.99 KB
tracing.html 124.55 KB 131.22 KB +6.67 KB (+5.4%)
debugger.html 141.83 KB 148.42 KB +6.59 KB (+4.6%)
domain.html 125.64 KB 131.41 KB +5.77 KB (+4.6%)
assets/jsx-runtime-BXRXL30K.js 5.67 KB — -5.67 KB (-100.0%)
assets/jsx-runtime-CW012A50.js — 5.67 KB +5.67 KB
wasi.html 79.05 KB 84.21 KB +5.16 KB (+6.5%)
punycode.html 63.62 KB 68.48 KB +4.86 KB (+7.6%)
querystring.html 65.74 KB 70.41 KB +4.67 KB (+7.1%)
string_decoder.html 64.78 KB 69.13 KB +4.34 KB (+6.7%)
permissions.html 88.75 KB 92.93 KB +4.18 KB (+4.7%)
assets/dist-D7-8NdKu.js 3.93 KB — -3.93 KB (-100.0%)
assets/dist-cDFsq_2f.js — 3.93 KB +3.93 KB
assets/CodeTabs-BCN1ZuaS.js 3.89 KB — -3.89 KB (-100.0%)
assets/CodeTabs-DT17uorw.js — 3.89 KB +3.89 KB
embedding.html 69.31 KB 73.14 KB +3.83 KB (+5.5%)
intl.html 55.12 KB 58.89 KB +3.77 KB (+6.8%)
typescript.html 49.24 KB 52.81 KB +3.57 KB (+7.3%)
assets/CodeBox-BU9A6SQ0.js 3.44 KB — -3.44 KB (-100.0%)
assets/CodeBox-BNILFdqT.js — 3.44 KB +3.44 KB
assets/hooks.module-DmVOR8gj.js 3.40 KB — -3.40 KB (-100.0%)
assets/hooks.module-BbOQEtt2.js — 3.40 KB +3.40 KB
environment_variables.html 36.40 KB 39.57 KB +3.17 KB (+8.7%)
documentation.html 27.26 KB 30.02 KB +2.76 KB (+10.1%)
index.html 25.45 KB 28.06 KB +2.61 KB (+10.2%)
assets/FunctionSignature-BkGQ3GtN.js 2.28 KB — -2.28 KB (-100.0%)
assets/FunctionSignature-BQoQjJ6P.js — 2.28 KB +2.28 KB
404.html 20.60 KB 22.80 KB +2.20 KB (+10.7%)
assets/Banner-DmiHcrIG.js 2.13 KB — -2.13 KB (-100.0%)
assets/Banner-Zf4myBWm.js — 2.13 KB +2.13 KB
assets/ChangeHistory-Py08-aa6.js 1.77 KB — -1.77 KB (-100.0%)
assets/ChangeHistory-C3DUcRCX.js — 1.77 KB +1.77 KB
assets/DataTag-8MiOm8lc.js 856.00 B — -856.00 B (-100.0%)
assets/DataTag-CTUSGgLj.js — 856.00 B +856.00 B
assets/DocumentationIndex-C41jjOOT.js 833.00 B — -833.00 B (-100.0%)
assets/DocumentationIndex-D_jFywVL.js — 833.00 B +833.00 B
assets/ArrowUpRightIcon-CeI6pEnL.js 618.00 B — -618.00 B (-100.0%)
assets/ArrowUpRightIcon-BSpTwAaB.js — 618.00 B +618.00 B
assets/Badge-Cd_pO_Vx.js 616.00 B — -616.00 B (-100.0%)
assets/Badge-CZF-RiB7.js — 616.00 B +616.00 B
assets/AlertBox-rhpuVHCX.js 591.00 B — -591.00 B (-100.0%)
assets/AlertBox-BNnsWt2b.js — 591.00 B +591.00 B
assets/CodeBracketIcon-NvhDSqX7.js 512.00 B — -512.00 B (-100.0%)
assets/CodeBracketIcon-TvXI-m3T.js — 512.00 B +512.00 B
assets/dist-BDAKpzg5.js 477.00 B — -477.00 B (-100.0%)
assets/dist-D1kGHM6c.js — 477.00 B +477.00 B
assets/ChevronDownIcon-CTgBO3tH.js 468.00 B — -468.00 B (-100.0%)
assets/ChevronDownIcon-BOnMJQ3A.js — 468.00 B +468.00 B
assets/renderLabel-_7gv3mHW.js 447.00 B — -447.00 B (-100.0%)
assets/renderLabel-CdEZo9LC.js — 447.00 B +447.00 B
assets/Blockquote-D4eEaruL.js 167.00 B — -167.00 B (-100.0%)
assets/Blockquote-C7yOM0SI.js — 167.00 B +167.00 B
assets/useRemoteConfig-BPIYr4xA.js 151.00 B — -151.00 B (-100.0%)
assets/useRemoteConfig-DRPTLZY3.js — 151.00 B +151.00 B
assets/withIsland-BKNTtLQ7.js 105.00 B — -105.00 B (-100.0%)
assets/withIsland-C7ctRfG5.js — 105.00 B +105.00 B

Performance estimate (single CI run)

  • Generation time: 44.5% faster (46.98 s → 26.08 s)
  • Peak memory: 45.3% lower (3.72 GB → 2.04 GB)

Comment thread packages/core/src/threading/services.mjs Outdated
Comment thread packages/core/src/threading/remote-host.mjs Outdated
@ovflowd

ovflowd commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Marking as draft because #1157 should be merged first.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

🟡 Changes recommended

Lazy embedded grammars, shared page mutation, and the breaking minor release classification need correction.

3 open findings
2 resolved since last review
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

🧠 Review effort: Balanced

Comment thread .changeset/build-memory.md
Comment thread packages/react/src/html/generate.mjs Outdated
Comment thread packages/core/src/plugins/shiki/highlighter.mjs
ovflowd and others added 14 commits October 12, 2026 17:20
The Shiki plugin registered every bundled language (~250 grammars) in
each thread that highlighted code, which cost ~2s and ~100MB per thread
and made every highlight several times slower, as each code block was
matched against grammars it never uses. Importing it also imported all
of them, through `@node-core/rehype-shiki`'s `LANGS` and its plugin,
on every thread loading `jsx-ast`, the main thread included.

The highlighter now registers a bundled language the first time code in
it is highlighted, along with the bundled languages a configured one
embeds, and lists the bundled ones from their metadata alone, without
importing `LANGS`. The themes are given to Shiki by name, which it keeps
parsed instead of parsing them for every highlight, named after the
configured themes, as Shiki loads a theme given for both schemes once.

A bundled language is registered with the bundled languages it embeds,
lazily too (a fenced code block in Markdown): Shiki only highlights
those once they are registered, so registering them later would make
code highlight differently depending on what was highlighted before it.
For the same reason, a bundled grammar injecting into others (`ts-tags`,
into JavaScript's tagged templates) only injects into the language it
was imported for.

`@node-core/rehype-shiki` is updated to 1.5.1, whose plugin only imports
every grammar when it isn't given a highlighter (nodejs/nodejs.org#9212).

The grammars now come from doc-kit's own `shiki` dependency (4.4.3),
which 1.5.1 shares, rather than the copy 1.5.0 pinned (4.3.1). Its C++
grammar highlights types and template arguments differently, which
shows on the Node.js docs' C++ examples.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Aviv Keller <me@aviv.sh>
Every highlighted token of every code block, signature and type became
a hast element, then a JSX element, then generated code, only to be
rendered back into the same markup: most of what `jsx-ast` allocated,
and much of what the pages' code took to compile and render. Highlighted
code is static, as islands adopt it without rendering it again, so it
now reaches the pages as HTML, written by `hast-util-to-html` and held
by a `<code>` through `dangerouslySetInnerHTML`.

Code blocks are embedded by a plugin running after Shiki. Types and
signatures are embedded as they are highlighted, before `rehype-raw`,
which would otherwise parse each of their tokens again.

It is the HTML Preact rendered, spelled differently in places:
`hast-util-to-html` writes `<` as `&#x3C;`, leaves `"` unescaped in
text, and keeps Shiki's token styles without a trailing `;`.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
The pool renders a single task on the calling thread, so `all.html`,
rendered on its own after the other pages, was rendered on the main
thread. That held the whole site in its heap, and kept the pool from
shutting its idle workers down until it was done. It is now rendered
alongside the other pages, first, as it takes by far the longest.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
The main thread loads every generator of a run, but only ever calls
`generate`; building the pages is the workers' job. Still, the
generators imported what only their workers use, so the main thread
loaded the TypeScript parser (WASM) and what building a page takes.
Those are now imported where they are used.

`getFullName` also moves to a module of its own: `buildBarProps`, which
`section-pages` imports, and `buildContent` only need a page's full
name, not the code that builds and highlights signatures.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
The threads rendering pages load the `html` generator's module, and so
everything `generate` imports, along with `processing.mjs`. That
included `config.mjs`, which loads Shiki for the languages' display
names and a Markdown processor of its own: ~14MB and ~75ms per worker,
for what only the main thread uses, while the workers render pages,
which is when the build peaks.

`createVirtualImports` moves into `config.mjs`, which `generate` imports
when it bundles the site, so the main thread alone loads it.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
A worker that highlights code no longer registers every grammar Shiki
bundles, only those of the code it highlighted. The comment now says
what holds for every generator: each worker has a heap of its own, with
the libraries and the pages it is working on.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
V8 lets a heap grow to several times its live data before collecting
it, the more the higher its limit: four times from 2GB up, and on a
machine with plenty of memory every worker's limit is 4GB, so each held
several times what it was using.

Each worker's old space is now limited to `WORKER_HEAP_SIZE` (512MB).
The biggest page of the Node.js docs, `all.html`, takes ~300MB. Node's
`--max-old-space-size` replaces the limit, to raise it, and it's what
limits the main thread, which runs the generators with one thread.

On Node core's build with 4 threads, the peak goes from 2.02GB to
1.67GB on Node 26, and from 2.44GB to 2.19GB on Node 24, for 3-5% more
time spent collecting garbage. The build passes with workers limited to
as little as ~350MB.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Aviv Keller <me@aviv.sh>
A worker only ended a second after its last task, so the `jsx-ast`
workers and their heaps (~800MB on the Node.js docs) were still there
while the main thread bundled the site, which is when the build peaked.
They now end a quarter of a second after running out of work, soon
enough to be gone while the site is bundled.

On Node core's build with 4 threads on Node 26, the peak goes from
~2.31GB to ~1.91GB. Workers also end in some of the gaps between
generators, so the build starts 10 or 11 workers rather than 8, which
costs ~0.3s.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Of the ~20,000 types the Node.js docs highlight, 741 are different:
`{string}` alone is on most pages. Each thread now keeps the markup of
the types it highlighted, by highlighter, so a type it has seen is only
embedded again, saving ~4% of the build's time and ~5% of its CPU.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Every thread loads the logger, and with it each transport: the GitHub
one imported `@actions/core`, and through it `undici`, for the four
functions printing workflow commands. That was 237 modules, 19MB and
~80ms on every thread, whichever transport is used. The transport now
writes the commands itself, escaped as `@actions/core` does, for the
same output: 10 modules, 3MB and 7ms.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
`generate` bundled the site and compiled the pages' programs in its own
scope, so everything it set up for that, the bundler, the program
builder and the compile function, stayed reachable while the pages
rendered. With one thread, the main thread renders them all, and held
it all the while.

The bundling and the compiling move to `buildPrograms`, which returns
the pages' tasks for the pool, so all of it is let go once it returns.

On Node core's build with one thread on Node 26, the peak goes from
~2.66GB to ~2.45GB (medians of 8 runs). With four threads, where the
workers render the pages, it doesn't change.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
It will be removed soon.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
The HTML generators minified every page with SWC's HTML minifier,
compiled to WASM. Its memory grows to about twelve times the biggest
page it is given, and is never returned: each thread rendering pages
kept it until it ended, and on Node.js 24, the worker that minified the
Node.js docs' 33MB `all.html` collected garbage on nearly every
allocation from then on, as V8 found that memory too large for its
heap limit. Minifying also took a fifth of the build's CPU.

For that, the Node.js docs' pages were 5.9% smaller, but only 3.2%
once gzipped and 2.1% with brotli, which is how hosts serve them:
Cloudflare, Vercel, Netlify and GitHub Pages compress HTML by default.
The pages are now written as rendered, and `@swc/html-wasm` is gone,
along with `@doc-kit/core/utils/html-minifier.mjs`. `minify` still
minifies scripts, styles and JSON, and the publishing docs now explain
how to serve the pages compressed.

On Node core's build with 4 threads on Node.js 26, the build takes 7.7s
instead of 10.5s and peaks at ~1.68GB instead of ~2.12GB; with one
thread, 15.2s instead of 22.5s. On Node.js 24, 7.9s instead of 51.7s.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Aviv Keller <me@aviv.sh>

This branch was successfully deployed

1 active deployment
Preview – api-docs-tooling — d4a1b61e Deployed Oct 12, 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.

4 participants