Skip to content

Commit e074c41

Browse files
committed
perf: don't minify by default
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 by default. So `minify` now defaults to `false`, the minifier is only loaded when it is enabled, and the publishing docs explain how to serve the output compressed. The `html` generator's scripts and styles stay minified, unless `vite.build.minify` says otherwise. 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>
1 parent 5d9cbb0 commit e074c41

13 files changed

Lines changed: 60 additions & 40 deletions

File tree

‎docs/configuration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Everything under the `global` key applies to every generator:
109109
logos, and templated text. Defaults to the `name` in your `package.json`.
110110
- `version` {string|SemVer} Documentation version. **Default:**
111111
`process.version`.
112-
- `minify` {boolean} Whether to minify output. **Default:** `true`.
112+
- `minify` {boolean} Whether to minify output. **Default:** `false`.
113113
- `repository` {string} GitHub repository in `owner/repo` format; without
114114
one, repository UI is omitted.
115115
- `ref` {string} Git reference (branch, tag, or commit SHA). **Default:**

‎docs/publishing.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ costs a request per asset each time a new tab opens the site. On Vercel:
5656
Within a visit, moving between pages loads no assets at all: the site
5757
navigates client-side, swapping in the next page's content.
5858

59+
## Compress the pages
60+
61+
doc-kit doesn't minify its output by default: compressed (gzip or brotli), as
62+
hosts serve it, it is far smaller than minifying would make it.
63+
64+
Cloudflare, Vercel, Netlify, and GitHub Pages compress on their own, and so
65+
does `npx serve out`. If you serve the output yourself, turn compression on:
66+
67+
- **nginx:** `gzip on;` compresses HTML; add the other types with
68+
`gzip_types text/css application/javascript application/json;`.
69+
- **Caddy:** `encode zstd gzip`.
70+
- **Apache:** with `mod_deflate`,
71+
`AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json`.
72+
- **Amazon S3:** serve the bucket through CloudFront, with **Compress objects
73+
automatically** on.
74+
5975
## Tell doc-kit its public URL
6076

6177
Set `baseURL` to where the site will live. Generators that emit absolute

‎packages/core/src/generators/json-all/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ generator's schema for the documents.
2323

2424
- `output` {string} The directory where `all.json` will be written.
2525
- `minify` {boolean} Whether to minify the output. Inherited from `global`.
26-
**Default:** `true`.
26+
**Default:** `false`.
2727
- `index` {Array} The `{ api }` objects defining the document order. Inherited
2828
from `global`.
2929
- `schemaURL` {string} Where the bundle's schema is published.

‎packages/core/src/generators/json-simple/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ The `json-simple` generator creates a simplified JSON version of the API documen
66

77
- `output` {string} The directory where `api-docs.json` will be written.
88
- `minify` {boolean} Whether to minify the output JSON. Inherited from
9-
`global`. **Default:** `true`.
9+
`global`. **Default:** `false`.

‎packages/core/src/generators/json/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Files keep the input's directory layout: `doc/api/some/fs.md` becomes `out/some/
1818

1919
- `output` {string} The directory to write the documents to.
2020
- `minify` {boolean} Whether to minify the output. Inherited from `global`.
21-
**Default:** `true`.
21+
**Default:** `false`.
2222
- `repository` {string} The `owner/name` repository source links resolve
2323
against. Inherited from `global`; without one, `sourceLink.url` is `null`.
2424
- `schemaURL` {string} Where the schema is published. `{schemaVersion}` is

‎packages/core/src/utils/configuration/index.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const getDefaultConfig = (generators, config) =>
6262
global: {
6363
project: detectProject() ?? 'API Docs',
6464
version: process.version,
65-
minify: true,
65+
minify: false,
6666
ref: 'HEAD',
6767
// Without release history there is nothing to build a version picker
6868
// from, so generators render single-version output.

‎packages/node-legacy/src/legacy-html-all/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ The `legacy-html-all` generator creates a single `all.html` file containing all
88
- `templatePath` {string} Path to the HTML template file. Inherited from
99
`legacy-html`. **Default:** `'template.html'`.
1010
- `minify` {boolean} Whether to minify the output HTML. Inherited from
11-
`global`. **Default:** `true`.
11+
`global`. **Default:** `false`.
1212
- `version` {Object} Version object containing version information. Inherited
1313
from `global`. **Default:** `process.version`.

‎packages/node-legacy/src/legacy-html/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ The `legacy-html` generator creates legacy HTML documentation pages for Node.js
1818
- `index` {Array} Array of `{ api, section }` objects defining the
1919
documentation structure.
2020
- `minify` {boolean} Whether to minify the output HTML. Inherited from
21-
`global`. **Default:** `true`.
21+
`global`. **Default:** `false`.

‎packages/node/src/api-links/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ The `api-links` generator creates a mapping of publicly accessible functions to
88
- `sourceURL` {string} URL template for linking to source files.
99
**Default:** `'${GITHUB_BLOB_URL}lib/{fileName}'`.
1010
- `minify` {boolean} Whether to minify the output JSON. Inherited from
11-
`global`. **Default:** `true`.
11+
`global`. **Default:** `false`.

‎packages/react/src/html/__tests__/generate.test.mjs‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,21 @@ describe('web generate', () => {
102102
]);
103103

104104
assert.match(fsHTML, /View As/);
105-
assert.match(fsHTML, /href=fs\.json/);
106-
assert.match(fsHTML, /href=fs\.md/);
105+
assert.match(fsHTML, /href="fs\.json"/);
106+
assert.match(fsHTML, /href="fs\.md"/);
107107
assert.doesNotMatch(notFoundHTML, /View As/);
108108
// Assets resolve from the page: relative for real pages, from the root
109109
// for synthetic ones, which are served at any path
110-
assert.match(fsHTML, /src=\.\.\/assets\/client-[^ ]+\.js/);
111-
assert.match(fsHTML, /href=\.\.\/assets\/[^ ]+\.css/);
112-
assert.match(notFoundHTML, /src=\/assets\/client-[^ ]+\.js/);
113-
assert.match(fsHTML, /on:idle[^>]*data-island-name=SearchBox/);
110+
assert.match(fsHTML, /src="\.\.\/assets\/client-[^"]+\.js"/);
111+
assert.match(fsHTML, /href="\.\.\/assets\/[^"]+\.css"/);
112+
assert.match(notFoundHTML, /src="\/assets\/client-[^"]+\.js"/);
113+
assert.match(fsHTML, /on:idle[^>]*data-island-name="SearchBox"/);
114114
// The manifest the asset tags were read from does not ship
115115
assert.equal((await readdir(output)).includes('.vite'), false);
116116

117117
// Fonts are hashed like every other asset, and preloaded from where the
118118
// bundler actually wrote them
119-
const fonts = [...fsHTML.matchAll(/href=\.\.\/(assets\/[^ ]+\.woff2)/g)];
119+
const fonts = [...fsHTML.matchAll(/href="\.\.\/(assets\/[^"]+\.woff2)"/g)];
120120
const written = await readdir(join(output, 'assets'));
121121

122122
assert.strictEqual(fonts.length, FONTS.length);
@@ -150,7 +150,7 @@ describe('web generate', () => {
150150
assert.match(html, /File system body[\s\S]*Zlib body/);
151151
assert.doesNotMatch(html, /Index body/);
152152
// Their tables of contents, concatenated
153-
assert.match(html, /href=#fs[\s\S]*href=#zlib/);
153+
assert.match(html, /href="#fs"[\s\S]*href="#zlib"/);
154154
assert.doesNotMatch(html, /View As/);
155155
});
156156

@@ -209,30 +209,30 @@ describe('web generate', () => {
209209
]);
210210

211211
// Assets and module files resolve from the nested directory
212-
assert.match(readFileHTML, /src=\.\.\/assets\//);
213-
assert.match(readFileHTML, /href=\.\.\/fs\.json/);
214-
assert.match(readFileHTML, /href=\.\.\/fs\.html#fsreadfile/);
212+
assert.match(readFileHTML, /src="\.\.\/assets\//);
213+
assert.match(readFileHTML, /href="\.\.\/fs\.json"/);
214+
assert.match(readFileHTML, /href="\.\.\/fs\.html#fsreadfile"/);
215215

216216
// The sidebar nests the module's sections, repeating the module itself
217217
assert.match(readFileHTML, /<details[^>]*open/);
218218
assert.match(
219219
readFileHTML,
220-
/href=\.\.\/fs\.html[^>]*>(<[^>]*>)*File system/
220+
/href="\.\.\/fs\.html"[^>]*>(<[^>]*>)*File system/
221221
);
222-
assert.match(readFileHTML, /href=callback-api\.html/);
222+
assert.match(readFileHTML, /href="callback-api\.html"/);
223223

224224
// Previous/next step through the module's sections
225225
assert.match(readFileHTML, /Callback API/);
226-
assert.match(readFileHTML, /href=watch\.html/);
226+
assert.match(readFileHTML, /href="watch\.html"/);
227227
assert.match(fsHTML, /Next/);
228228

229229
// Links were re-targeted for the chunk page
230-
assert.match(readFileHTML, /href=watch\.html#fswatch/);
231-
assert.match(readFileHTML, /href=\.\.\/net\.html/);
230+
assert.match(readFileHTML, /href="watch\.html#fswatch"/);
231+
assert.match(readFileHTML, /href="\.\.\/net\.html"/);
232232

233233
// The full page is untouched, and lists sections in its own sidebar
234-
assert.match(fsHTML, /href=fs\.json/);
235-
assert.match(fsHTML, /href=fs\/readFile\.html/);
234+
assert.match(fsHTML, /href="fs\.json"/);
235+
assert.match(fsHTML, /href="fs\/readFile\.html"/);
236236
});
237237

238238
it('renders the configurable head without hardcoded defaults', async context => {
@@ -252,10 +252,10 @@ describe('web generate', () => {
252252

253253
assert.match(html, /Custom project docs/);
254254
assert.match(html, /https:\/\/example\.com\/og\.png/);
255-
assert.match(html, /href=https:\/\/example\.com\/favicon\.ico/);
256-
assert.match(html, /content=#abcdef/);
255+
assert.match(html, /href="https:\/\/example\.com\/favicon\.ico"/);
256+
assert.match(html, /content="#abcdef"/);
257257
assert.doesNotMatch(html, /nodejs\.org/);
258-
assert.match(html, /property=og:type content=website/);
258+
assert.match(html, /property="og:type" content="website"/);
259259
});
260260

261261
it('uses the base URL for absolute client assets', async context => {
@@ -268,8 +268,8 @@ describe('web generate', () => {
268268
await generate([toPage(content)]);
269269
const html = await readFile(join(output, '404.html'), 'utf8');
270270

271-
assert.match(html, /src=https:\/\/example\.com\/docs\/assets\//);
272-
assert.match(html, /href=https:\/\/example\.com\/docs\/assets\//);
271+
assert.match(html, /src="https:\/\/example\.com\/docs\/assets\//);
272+
assert.match(html, /href="https:\/\/example\.com\/docs\/assets\//);
273273
});
274274

275275
it('applies configured Vite plugins to the client build', async context => {
@@ -367,18 +367,18 @@ describe('web generate', () => {
367367
assert.match(html, /<article data-custom-ssr>fs:1<\/article>/);
368368
assert.match(
369369
html,
370-
/<script type=module crossorigin src=\.\/custom\/index\.js>/
370+
/<script type="module" crossorigin src="\.\/custom\/index\.js">/
371371
);
372372
assert.match(html, /data-router/);
373373
// The adapter reported no fonts, so there are none to preload
374-
assert.doesNotMatch(html, /as=font/);
374+
assert.doesNotMatch(html, /as="font"/);
375375
assert.match(
376376
html,
377-
/<link rel=modulepreload crossorigin href=\.\/custom\/shared\.js>/
377+
/<link rel="modulepreload" crossorigin href="\.\/custom\/shared\.js" \/>/
378378
);
379379
assert.match(
380380
html,
381-
/<link rel=stylesheet crossorigin href=\.\/custom\/index\.css>/
381+
/<link rel="stylesheet" crossorigin href="\.\/custom\/index\.css" \/>/
382382
);
383383
assert.deepStrictEqual(calls, ['server', 'client', 'compile']);
384384
});

0 commit comments

Comments
 (0)