Skip to content

Commit

Permalink
feat(core): faster HTML minimizer - `siteConfig.future.experimental_f…
Browse files Browse the repository at this point in the history
…aster.swcHtmlMinimizer` (#10554)
  • Loading branch information
slorber authored Oct 4, 2024
1 parent 126d395 commit 912c495
Show file tree
Hide file tree
Showing 16 changed files with 416 additions and 102 deletions.
1 change: 1 addition & 0 deletions packages/docusaurus-bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"postcss": "^8.4.26",
"postcss-loader": "^7.3.3",
"file-loader": "^6.2.0",
"html-minifier-terser": "^7.2.0",
"mini-css-extract-plugin": "^2.9.1",
"null-loader": "^4.0.1",
"react-dev-utils": "^12.0.1",
Expand Down
13 changes: 11 additions & 2 deletions packages/docusaurus-bundler/src/importFaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import type {
} from 'terser-webpack-plugin';
import type {MinimizerOptions as CssMinimizerOptions} from 'css-minimizer-webpack-plugin';

async function importFaster() {
type FasterModule = Awaited<typeof import('@docusaurus/faster')>;

async function importFaster(): Promise<FasterModule> {
return import('@docusaurus/faster');
}

async function ensureFaster() {
async function ensureFaster(): Promise<FasterModule> {
try {
return await importFaster();
} catch (error) {
Expand All @@ -41,6 +43,13 @@ export async function importSwcJsMinimizerOptions(): Promise<
return faster.getSwcJsMinimizerOptions() as JsMinimizerOptions<CustomOptions>;
}

export async function importSwcHtmlMinifier(): Promise<
ReturnType<FasterModule['getSwcHtmlMinifier']>
> {
const faster = await ensureFaster();
return faster.getSwcHtmlMinifier();
}

export async function importLightningCssMinimizerOptions(): Promise<
CssMinimizerOptions<CustomOptions>
> {
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-bundler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export {
} from './currentBundler';

export {getMinimizers} from './minification';
export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml';
export {createJsLoaderFactory} from './loaders/jsLoader';
export {createStyleLoadersFactory} from './loaders/styleLoader';
148 changes: 148 additions & 0 deletions packages/docusaurus-bundler/src/minifyHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import logger from '@docusaurus/logger';
import {minify as terserHtmlMinifier} from 'html-minifier-terser';
import {importSwcHtmlMinifier} from './importFaster';
import type {DocusaurusConfig} from '@docusaurus/types';

// Historical env variable
const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true';

export type HtmlMinifier = {
minify: (html: string) => Promise<string>;
};

const NoopMinifier: HtmlMinifier = {
minify: async (html: string) => html,
};

type SiteConfigSlice = {
future: {
experimental_faster: Pick<
DocusaurusConfig['future']['experimental_faster'],
'swcHtmlMinimizer'
>;
};
};

export async function getHtmlMinifier({
siteConfig,
}: {
siteConfig: SiteConfigSlice;
}): Promise<HtmlMinifier> {
if (SkipHtmlMinification) {
return NoopMinifier;
}
if (siteConfig.future.experimental_faster.swcHtmlMinimizer) {
return getSwcMinifier();
} else {
return getTerserMinifier();
}
}

// Minify html with https://github.com/DanielRuf/html-minifier-terser
async function getTerserMinifier(): Promise<HtmlMinifier> {
return {
minify: async function minifyHtmlWithTerser(html) {
try {
return await terserHtmlMinifier(html, {
removeComments: false,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyJS: true,
});
} catch (err) {
throw new Error(`HTML minification failed (Terser)`, {
cause: err as Error,
});
}
},
};
}

// Minify html with @swc/html
// Not well-documented but fast!
// See https://github.com/swc-project/swc/discussions/9616
async function getSwcMinifier(): Promise<HtmlMinifier> {
const swcHtmlMinifier = await importSwcHtmlMinifier();
return {
minify: async function minifyHtmlWithSwc(html) {
try {
const result = await swcHtmlMinifier(Buffer.from(html), {
// Removing comments can lead to React hydration errors
// See https://x.com/sebastienlorber/status/1841966927440478577
removeComments: false,
// TODO maybe it's fine to only keep <!-- --> React comments?
preserveComments: [],

// Sorting these attributes (class) can lead to React hydration errors
sortSpaceSeparatedAttributeValues: false,
sortAttributes: false,

// @ts-expect-error: bad type https://github.com/swc-project/swc/pull/9615
removeRedundantAttributes: 'all',
removeEmptyAttributes: true,
minifyJs: true,
minifyJson: true,
minifyCss: true,
});

// Escape hatch because SWC is quite aggressive to report errors
// TODO figure out what to do with these errors: throw or swallow?
// See https://github.com/facebook/docusaurus/pull/10554
// See https://github.com/swc-project/swc/discussions/9616#discussioncomment-10846201
const ignoreSwcMinifierErrors =
process.env.DOCUSAURUS_IGNORE_SWC_HTML_MINIFIER_ERRORS === 'true';
if (!ignoreSwcMinifierErrors && result.errors) {
const ignoredErrors: string[] = [
// TODO Docusaurus seems to emit NULL chars, and minifier detects it
// see https://github.com/facebook/docusaurus/issues/9985
'Unexpected null character',
];
result.errors = result.errors.filter(
(diagnostic) => !ignoredErrors.includes(diagnostic.message),
);
if (result.errors.length) {
throw new Error(
`HTML minification diagnostic errors:
- ${result.errors
.map(
(diagnostic) =>
`[${diagnostic.level}] ${
diagnostic.message
} - ${JSON.stringify(diagnostic.span)}`,
)
.join('\n- ')}
Note: please report the problem to the Docusaurus team
In the meantime, you can skip this error with ${logger.code(
'DOCUSAURUS_IGNORE_SWC_HTML_MINIFIER_ERRORS=true',
)}`,
);
}
/*
if (result.errors.length) {
throw new AggregateError(
result.errors.map(
(diagnostic) => new Error(JSON.stringify(diagnostic, null, 2)),
),
);
}
*/
}
return result.code;
} catch (err) {
throw new Error(`HTML minification failed (SWC)`, {
cause: err as Error,
});
}
},
};
}
3 changes: 2 additions & 1 deletion packages/docusaurus-faster/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
},
"license": "MIT",
"dependencies": {
"@swc/core": "^1.7.14",
"@swc/core": "^1.7.28",
"@swc/html": "^1.7.28",
"browserslist": "^4.24.0",
"lightningcss": "^1.27.0",
"swc-loader": "^0.2.6",
Expand Down
5 changes: 5 additions & 0 deletions packages/docusaurus-faster/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@

import * as lightningcss from 'lightningcss';
import browserslist from 'browserslist';
import {minify as swcHtmlMinifier} from '@swc/html';
import type {RuleSetRule} from 'webpack';
import type {JsMinifyOptions} from '@swc/core';

export function getSwcHtmlMinifier(): typeof swcHtmlMinifier {
return swcHtmlMinifier;
}

export function getSwcJsLoaderFactory({
isServer,
}: {
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-types/src/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export type StorageConfig = {
export type FasterConfig = {
swcJsLoader: boolean;
swcJsMinimizer: boolean;
swcHtmlMinimizer: boolean;
lightningCssMinimizer: boolean;
mdxCrossCompilerCache: boolean;
rspackBundler: boolean;
Expand Down
3 changes: 1 addition & 2 deletions packages/docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@
"eta": "^2.2.0",
"eval": "^0.1.8",
"fs-extra": "^11.1.1",
"html-minifier-terser": "^7.2.0",
"html-tags": "^3.3.1",
"html-webpack-plugin": "^5.5.3",
"html-webpack-plugin": "^5.6.0",
"leven": "^3.1.0",
"lodash": "^4.17.21",
"p-map": "^4.0.0",
Expand Down
18 changes: 12 additions & 6 deletions packages/docusaurus/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import fs from 'fs-extra';
import path from 'path';
import _ from 'lodash';
import {compile} from '@docusaurus/bundler';
import {compile, getHtmlMinifier} from '@docusaurus/bundler';
import logger, {PerfLogger} from '@docusaurus/logger';
import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils';
import {loadSite, loadContext, type LoadContextParams} from '../server/site';
Expand Down Expand Up @@ -271,17 +271,23 @@ async function executeSSG({
return {collectedData: {}};
}

const renderer = await PerfLogger.async('Load App renderer', () =>
loadAppRenderer({
serverBundlePath,
}),
);
const [renderer, htmlMinifier] = await Promise.all([
PerfLogger.async('Load App renderer', () =>
loadAppRenderer({
serverBundlePath,
}),
),
PerfLogger.async('Load HTML minifier', () =>
getHtmlMinifier({siteConfig: props.siteConfig}),
),
]);

const ssgResult = await PerfLogger.async('Generate static files', () =>
generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params,
htmlMinifier,
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -80,6 +81,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -148,6 +150,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -216,6 +219,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -284,6 +288,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -352,6 +357,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -420,6 +426,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -490,6 +497,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -560,6 +568,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down Expand Up @@ -633,6 +642,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
"lightningCssMinimizer": false,
"mdxCrossCompilerCache": false,
"rspackBundler": false,
"swcHtmlMinimizer": false,
"swcJsLoader": false,
"swcJsMinimizer": false,
},
Expand Down
Loading

0 comments on commit 912c495

Please sign in to comment.