Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): faster HTML minimizer - siteConfig.future.experimental_faster.swcHtmlMinimizer #10554

Merged
merged 7 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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