Skip to content

perf: highlight with fewer grammars by default - #1235

Open
ovflowd wants to merge 4 commits into
mainfrom
perf/fewer-grammars
Open

ovflowd wants to merge 4 commits into
mainfrom
perf/fewer-grammars

Conversation

@ovflowd

@ovflowd ovflowd commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Description

This PR highlights code with fewer grammars by default, as each one takes its own memory on every thread, and time to register. JavaScript (js, mjs, cjs, javascript) now uses TypeScript's grammar, which already highlights types, and C++ (cpp, c++) uses C's. JavaScript's grammar alone is ~60MB on every thread: on Node.js's docs the site build peaks 95MB lower at 4 threads and 207MB lower at 6, and builds take 1-6% less time.

In the light theme JavaScript comes out the same, except one block of console output marked as js. Nord, the dark theme, loses a few colors it only gives JavaScript's scopes, and C++-only keywords lose theirs. langAlias takes precedence, so { cpp: 'cpp', 'c++': 'cpp' } brings C++'s own grammar back.

doc-kit now resolves aliases itself, since Shiki replaced configured ones with a grammar's own as it registered. Aliases also follow other aliases and match in any case. resolveLanguage returns, and transformers see, the language code is highlighted as (typescript for mjs).

Why it's built this way

What's wrong with JavaScript's grammar? Nothing, we just pay for it twice. Each worker has its own Shiki instance and Oniguruma engine, so every grammar a thread registers costs that thread its own memory. Grammars are already registered lazily, but Node.js's docs have 2,632 js/mjs/cjs blocks spread over nearly every page, so every thread ended up with JavaScript's grammar next to TypeScript's, which types are highlighted with anyway. There are only 5 ts blocks.

Doesn't JavaScript's grammar still load? On some threads, yes. Embedded grammars load with the grammar that embeds them: http (http.md, http2.md) embeds JavaScript through graphql, and coffee (module.md) embeds it directly. So the saving is on every thread that doesn't render those three pages.

And C++? The docs have 29 cpp blocks and 211 c ones. C++'s grammar brings cpp-macro, glsl and regexp with it, ~850KB of grammar source against 78KB for C, which is loaded anyway for the c blocks.

Why resolve aliases in doc-kit and not with Shiki's langAlias? When a grammar registers, Shiki overwrites the alias map with that grammar's own aliases (this._alias[alias] = lang.name). With js: 'typescript' set in Shiki, the first http or coffee block would register JavaScript's grammar and flip js back to it in the middle of a build, so a page would highlight differently depending on what its thread had highlighted before. resolveLanguage returns typescript because Shiki only knows TypeScript as ts, cts and mts, so callers have to highlight with the name it returns.

Why does a configured alias turn the defaults off? So getting a grammar back doesn't need a new option. With { cpp: 'cpp' }, a cpp block stops at the repeat and gets C++'s grammar. The defaults only apply when no configured alias matched.

Why lowercase? main already lowercased the language before Shiki's lookup, and Shiki keeps its bundled names and aliases in lowercase. Configured keys are lowercased now too, so { MyConf: 'ini' } matches myconf as well.

Why does one thread use more memory on the site build? I don't know yet. On one thread, http.md and module.md register JavaScript's grammar anyway, so only C++'s grammars drop out and there's little to save. It went from 1747 to 1838MB (+5%); this PR ran 1834 to 1850MB and base ran 1742, 1747 and 1982MB, so with 3 runs it may well be real. The legacy build on one thread went down 87MB, but base was noisy there too (1136 to 1270MB).

Does it matter with the stack? The saving grows with threads, and #1243 raises the default to up to 6. The stack alone is +34% memory on the site build at 4 threads; with this PR and #1236 it's -20% / -22% time and +3% / -1% memory at 4 / 6 threads, against main before #1237 (61fa0b9). I haven't measured this PR alone on top of the stack.

Benchmarks

Node.js's docs on Node.js 26. This PR against its base (61fa0b9), medians of 5 interleaved runs (3 on one thread). Peak memory is RSS summed over the process tree. The site build is -t orama-db -t json -t llms-txt -t section-pages and the legacy build is -t legacy-html-all -t legacy-json-all.

Build Threads Time Peak memory
site 1 22.80s → 22.43s (-2%) 1747 → 1838MB (+5%)
site 4 9.25s → 8.99s (-3%) 1797 → 1702MB (-5%)
site 6 8.50s → 8.35s (-2%) 2216 → 2009MB (-9%)
legacy 1 9.26s → 8.91s (-4%) 1260 → 1173MB (-7%)
legacy 4 4.24s → 4.00s (-6%) 1539 → 1646MB (+7%)
legacy 6 4.20s → 4.17s (-1%) 1972 → 1873MB (-5%)

The legacy build at 4 threads went up, and it doesn't look like noise: 4 of 5 runs (1637 to 1674MB) were above every base run (1526 to 1603MB). I haven't found why yet. At 6 threads the same build is 5% lower.

Validation

Added highlights JavaScript as TypeScript, and C++ as C, takes configured aliases over the default ones, follows configured aliases, and finds languages named in any case and highlights with a language of `langs` an alias leads to. node --run test, lint and format:check are clean.

The memory and time are from the benchmarks above.

Related Issues

Split out of #1205.

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 Nov 3, 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 Nov 11, 2026 12:07am UTC

Request Review

@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.23%. Comparing base (61fa0b9) to head (b570722).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1235      +/-   ##
==========================================
+ Coverage   94.13%   94.23%   +0.09%     
==========================================
  Files         291      292       +1     
  Lines       28900    29331     +431     
  Branches     2932     2987      +55     
==========================================
+ Hits        27206    27640     +434     
+ Misses       1672     1669       -3     
  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.

@ovflowd ovflowd mentioned this pull request Nov 3, 2026
4 tasks done
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 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 ↗ b570722 2026-11-11T00:08:49.230Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ 15a06e7 2026-11-06T19:44:14.119Z View logs ↗
  • Build: In progress 🔵

View logs ↗
891436d 2026-11-06T19:30:49.437Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ 269a455 2026-11-06T13:35:43.503Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ ad1a970 2026-11-04T18:24:22.829Z View logs ↗
  • Build: Success ✅

View logs ↗
No Preview URL. Enable ↗ 2b215f2 2026-11-03T19:34:36.267Z View logs ↗

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

api-links Generator

Performance estimate (single CI run)

  • Generation time: 17.1% slower (820.00 ms → 960.00 ms)
  • Peak memory: 2.3% lower (283.63 MB → 277.02 MB)

json Generator

Performance estimate (single CI run)

  • Generation time: 50.8% slower (5.98 s → 9.02 s)
  • Peak memory: 14.1% lower (1.00 GB → 879.44 MB)

legacy-html Generator

Output size: 10 files changed · net -52.57 KB

File size details
File Main PR Change
addons.html 374.23 KB 328.87 KB -45.36 KB (-12.1%)
embedding.html 64.38 KB 57.78 KB -6.60 KB (-10.3%)
async_context.html 247.17 KB 245.10 KB -2.08 KB (-0.8%)
tls.html 297.33 KB 298.88 KB +1.54 KB (+0.5%)
n-api.html 822.59 KB 822.23 KB -364.00 B (-0.0%)
module.html 364.63 KB 364.84 KB +224.00 B (+0.1%)
readline.html 219.10 KB 219.27 KB +168.00 B (+0.1%)
worker_threads.html 417.80 KB 417.64 KB -168.00 B (-0.0%)
repl.html 226.64 KB 226.75 KB +112.00 B (+0.0%)
test.html 1009.73 KB 1009.67 KB -56.00 B (-0.0%)

Performance estimate (single CI run)

  • Generation time: 16.7% faster (11.52 s → 9.60 s)
  • Peak memory: 1.4% lower (1.43 GB → 1.42 GB)

legacy-json Generator

Performance estimate (single CI run)

  • Generation time: 4.4% slower (8.23 s → 8.59 s)
  • Peak memory: 13.0% lower (1015.46 MB → 883.19 MB)

llms-txt Generator

Performance estimate (single CI run)

  • Generation time: 27.9% faster (6.10 s → 4.40 s)
  • Peak memory: 10.0% higher (866.38 MB → 953.02 MB)

orama-db Generator

Performance estimate (single CI run)

  • Generation time: 12.2% faster (8.52 s → 7.48 s)
  • Peak memory: 0.6% higher (945.88 MB → 951.88 MB)

web Generator

Output size: 46 files changed · net -108.36 KB

File size details
File Main PR Change
all.html 34.83 MB 34.78 MB -54.18 KB (-0.2%)
addons.html 389.73 KB 361.93 KB -27.80 KB (-7.1%)
embedding.html 73.02 KB 68.39 KB -4.63 KB (-6.3%)
process.html 1.20 MB 1.19 MB -4.16 KB (-0.3%)
child_process.html 706.46 KB 704.22 KB -2.24 KB (-0.3%)
tls.html 550.17 KB 552.35 KB +2.18 KB (+0.4%)
fs.html 2.36 MB 2.35 MB -1.32 KB (-0.1%)
crypto.html 2.07 MB 2.06 MB -1.21 KB (-0.1%)
assert.html 673.00 KB 671.89 KB -1.11 KB (-0.2%)
http.html 1.23 MB 1.23 MB -1.06 KB (-0.1%)
util.html 1.26 MB 1.26 MB -964.00 B (-0.1%)
async_hooks.html 293.14 KB 292.26 KB -898.00 B (-0.3%)
http2.html 1.31 MB 1.31 MB -788.00 B (-0.1%)
readline.html 380.82 KB 380.05 KB -788.00 B (-0.2%)
buffer.html 1.82 MB 1.82 MB -736.00 B (-0.0%)
dgram.html 313.49 KB 312.78 KB -728.00 B (-0.2%)
vm.html 656.77 KB 656.11 KB -680.00 B (-0.1%)
webstreams.html 565.43 KB 564.77 KB -674.00 B (-0.1%)
perf_hooks.html 910.85 KB 910.24 KB -620.00 B (-0.1%)
cluster.html 317.74 KB 317.14 KB -616.00 B (-0.2%)
worker_threads.html 662.16 KB 661.58 KB -588.00 B (-0.1%)
ffi.html 216.87 KB 216.38 KB -504.00 B (-0.2%)
zlib.html 1.00 MB 1023.86 KB -504.00 B (-0.0%)
test.html 1.47 MB 1.47 MB -458.00 B (-0.0%)
globals.html 302.35 KB 301.96 KB -394.00 B (-0.1%)
vfs.html 213.60 KB 213.27 KB -340.00 B (-0.2%)
url.html 533.89 KB 533.56 KB -338.00 B (-0.1%)
stream_iter.html 795.19 KB 794.90 KB -296.00 B (-0.0%)
v8.html 574.47 KB 574.19 KB -294.00 B (-0.0%)
async_context.html 329.94 KB 329.71 KB -234.00 B (-0.1%)
n-api.html 1.05 MB 1.05 MB -230.00 B (-0.0%)
dns.html 442.03 KB 441.81 KB -224.00 B (-0.0%)
sqlite.html 542.98 KB 542.81 KB -174.00 B (-0.0%)
domain.html 131.23 KB 131.06 KB -168.00 B (-0.1%)
quic.html 986.03 KB 985.87 KB -168.00 B (-0.0%)
stream.html 1.27 MB 1.27 MB -122.00 B (-0.0%)
repl.html 314.01 KB 313.90 KB -116.00 B (-0.0%)
modules.html 238.01 KB 237.90 KB -112.00 B (-0.0%)
tty.html 113.74 KB 113.63 KB -112.00 B (-0.1%)
module.html 533.09 KB 533.01 KB -84.00 B (-0.0%)
bench.html 213.40 KB 213.34 KB -60.00 B (-0.0%)
cli.html 679.37 KB 679.31 KB -56.00 B (-0.0%)
permissions.html 95.17 KB 95.12 KB -56.00 B (-0.1%)
esm.html 196.06 KB 196.05 KB -8.00 B (-0.0%)
single-executable-applications.html 170.83 KB 170.83 KB -2.00 B (-0.0%)
wasi.html 83.98 KB 83.98 KB -2.00 B (-0.0%)

Performance estimate (single CI run)

  • Generation time: 14.5% slower (14.70 s → 16.83 s)
  • Peak memory: 3.3% lower (1.52 GB → 1.47 GB)

@ovflowd
ovflowd marked this pull request as ready for review November 3, 2026 20:39
@ovflowd
ovflowd requested a review from a team as a code owner November 3, 2026 20:39
kgeminicdev

This comment was marked as low quality.

@ovflowd

ovflowd commented Nov 4, 2026 •

Copy link
Copy Markdown
Member Author

Ack, the alias keys are lowercased now, with a test

Each grammar costs every thread its own memory, about 60MB for
JavaScript's on Oniguruma, and time to register. Types and signatures
are already highlighted as TypeScript, whose grammar highlights
JavaScript too, so `js`, `mjs`, `cjs` and `javascript` code is now
highlighted with it by default, and C++ (`cpp`, `c++`) with C's. On
Node.js's docs that saves ~190MB of peak memory and 2-7% of build time.

In the light theme, the default, JavaScript comes out the same but for
one block of console output marked as `js`. Nord, the dark theme, has
a few colors only for JavaScript's scopes. C++-only keywords lose their
color.

`langAlias` takes precedence, and doc-kit now resolves aliases itself:
Shiki replaced configured aliases with a grammar's own as it
registered. Aliases now also lead on through other aliases and are
found in any case. Transformers, and the language `resolveLanguage`
returns, are the one code is highlighted as.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Sonnet 5.5 <noreply@anthropic.com>
The lookup lowercased the language, but the aliases kept their keys as
written, so `{ MyConf: 'ini' }` never matched. They're keyed in
lowercase now. The test also says why Rust stands in for a bundled
language registered through its own alias.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
Following aliases always ended, since each one is followed once, but
the set of those followed kept the names as written while the lookup
lowercased them, so it took a second look to see. Both use the
lowercased name now, and the test adds a cycle that differs only in
case.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
The cycle differing only in case checked nothing the other cycle
doesn't: following aliases ended before tracking them by their
lowercased name too, which only made it easier to see. A capitalised
key is checked once, the conf alias is highlighted as ini in the test
of configured languages already, and the test of configured aliases
over the default ones keeps C++, as the test of a language of langs
checks JavaScript's. The comment on Rust no longer says "now".

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – api-docs-tooling — b5707229 Deployed Nov 11, 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