feat(core): faster JS minimizer - siteConfig.future.experimental_faster.swcJsMinimizer (#10441)

This commit is contained in:
Sébastien Lorber 2024-08-23 18:44:42 +02:00 committed by GitHub
parent aa65f39d8c
commit bb90e35153
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 202 additions and 36 deletions

View file

@ -6,6 +6,7 @@
*/
import type {RuleSetRule} from 'webpack';
import type {JsMinifyOptions} from '@swc/core';
export function getSwcJsLoaderFactory({
isServer,
@ -33,3 +34,24 @@ export function getSwcJsLoaderFactory({
},
};
}
// Note: these options are similar to what we use in core
// They should rather be kept in sync for now to avoid any unexpected behavior
// The goal of faster minifier is not to fine-tune options but only to be faster
// See core minification.ts
export function getSwcJsMinifierOptions(): JsMinifyOptions {
return {
ecma: 2020,
compress: {
ecma: 5,
},
module: true,
mangle: true,
safari10: true,
format: {
ecma: 5,
comments: false,
ascii_only: true,
},
};
}

View file

@ -125,6 +125,7 @@ export type StorageConfig = {
export type FasterConfig = {
swcJsLoader: boolean;
swcJsMinimizer: boolean;
};
export type FutureConfig = {

View file

@ -14,6 +14,7 @@ export {
ParseFrontMatter,
DocusaurusConfig,
FutureConfig,
FasterConfig,
StorageConfig,
Config,
} from './config';

View file

@ -92,7 +92,7 @@
"semver": "^7.5.4",
"serve-handler": "^6.1.5",
"shelljs": "^0.8.5",
"terser-webpack-plugin": "^5.3.9",
"terser-webpack-plugin": "^5.3.10",
"tslib": "^2.6.0",
"update-notifier": "^6.0.2",
"url-loader": "^4.1.1",

View file

@ -334,6 +334,7 @@ async function getBuildClientConfig({
const result = await createBuildClientConfig({
props,
minify: cliOptions.minify ?? true,
faster: props.siteConfig.future.experimental_faster,
bundleAnalyzer: cliOptions.bundleAnalyzer ?? false,
});
let {config} = result;

View file

@ -136,6 +136,7 @@ async function getStartClientConfig({
let {clientConfig: config} = await createStartClientConfig({
props,
minify,
faster: props.siteConfig.future.experimental_faster,
poll,
});
config = executePluginsConfigureWebpack({

View file

@ -6,6 +6,7 @@
*/
import type {ConfigureWebpackUtils} from '@docusaurus/types';
import type {MinimizerOptions, CustomOptions} from 'terser-webpack-plugin';
async function importFaster() {
return import('@docusaurus/faster');
@ -22,9 +23,16 @@ async function ensureFaster() {
}
}
export async function getSwcJsLoaderFactory(): Promise<
export async function importSwcJsLoaderFactory(): Promise<
ConfigureWebpackUtils['getJSLoader']
> {
const faster = await ensureFaster();
return faster.getSwcJsLoaderFactory;
}
export async function importSwcJsMinifierOptions(): Promise<
MinimizerOptions<CustomOptions>
> {
const faster = await ensureFaster();
return faster.getSwcJsMinifierOptions() as MinimizerOptions<CustomOptions>;
}

View file

@ -10,6 +10,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -74,6 +75,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -138,6 +140,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -202,6 +205,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -266,6 +270,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -330,6 +335,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -394,6 +400,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -460,6 +467,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -526,6 +534,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {
@ -595,6 +604,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {

View file

@ -80,6 +80,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
"future": {
"experimental_faster": {
"swcJsLoader": false,
"swcJsMinimizer": false,
},
"experimental_router": "browser",
"experimental_storage": {

View file

@ -47,6 +47,7 @@ describe('normalizeConfig', () => {
future: {
experimental_faster: {
swcJsLoader: true,
swcJsMinimizer: true,
},
experimental_storage: {
type: 'sessionStorage',
@ -741,6 +742,7 @@ describe('future', () => {
const future: DocusaurusConfig['future'] = {
experimental_faster: {
swcJsLoader: true,
swcJsMinimizer: true,
},
experimental_storage: {
type: 'sessionStorage',
@ -1200,5 +1202,75 @@ describe('future', () => {
`);
});
});
describe('swcJsMinimizer', () => {
it('accepts - undefined', () => {
const faster: Partial<FasterConfig> = {
swcJsMinimizer: undefined,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining({swcJsMinimizer: false}));
});
it('accepts - true', () => {
const faster: Partial<FasterConfig> = {
swcJsMinimizer: true,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining({swcJsMinimizer: true}));
});
it('accepts - false', () => {
const faster: Partial<FasterConfig> = {
swcJsMinimizer: false,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining({swcJsMinimizer: false}));
});
it('rejects - null', () => {
// @ts-expect-error: invalid
const faster: Partial<FasterConfig> = {swcJsMinimizer: 42};
expect(() =>
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_faster.swcJsMinimizer" must be a boolean
"
`);
});
it('rejects - number', () => {
// @ts-expect-error: invalid
const faster: Partial<FasterConfig> = {swcJsMinimizer: 42};
expect(() =>
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_faster.swcJsMinimizer" must be a boolean
"
`);
});
});
});
});

View file

@ -43,11 +43,13 @@ export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
export const DEFAULT_FASTER_CONFIG: FasterConfig = {
swcJsLoader: false,
swcJsMinimizer: false,
};
// When using the "faster: true" shortcut
export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = {
swcJsLoader: true,
swcJsMinimizer: true,
};
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
@ -212,6 +214,9 @@ const FASTER_CONFIG_SCHEMA = Joi.alternatives()
.try(
Joi.object<FasterConfig>({
swcJsLoader: Joi.boolean().default(DEFAULT_FASTER_CONFIG.swcJsLoader),
swcJsMinimizer: Joi.boolean().default(
DEFAULT_FASTER_CONFIG.swcJsMinimizer,
),
}),
Joi.boolean()
.required()

View file

@ -11,7 +11,10 @@ import _ from 'lodash';
import * as utils from '@docusaurus/utils/lib/webpackUtils';
import {posixPath} from '@docusaurus/utils';
import {excludeJS, clientDir, createBaseConfig} from '../base';
import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation';
import {
DEFAULT_FASTER_CONFIG,
DEFAULT_FUTURE_CONFIG,
} from '../../server/configValidation';
import type {Props} from '@docusaurus/types';
describe('babel transpilation exclude logic', () => {
@ -107,7 +110,12 @@ describe('base webpack config', () => {
it('creates webpack aliases', async () => {
const aliases = ((
await createBaseConfig({props, isServer: true, minify: true})
await createBaseConfig({
props,
isServer: true,
minify: true,
faster: DEFAULT_FASTER_CONFIG,
})
).resolve?.alias ?? {}) as {[alias: string]: string};
// Make aliases relative so that test work on all computers
const relativeAliases = _.mapValues(aliases, (a) =>
@ -123,7 +131,12 @@ describe('base webpack config', () => {
.spyOn(utils, 'getFileLoaderUtils')
.mockImplementation(() => fileLoaderUtils);
await createBaseConfig({props, isServer: false, minify: false});
await createBaseConfig({
props,
isServer: false,
minify: false,
faster: DEFAULT_FASTER_CONFIG,
});
expect(mockSvg).toHaveBeenCalled();
});
});

View file

@ -14,10 +14,10 @@ import {
getStyleLoaders,
getCustomBabelConfigFilePath,
} from './utils';
import {getMinimizer} from './minification';
import {getMinimizers} from './minification';
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
import type {Configuration} from 'webpack';
import type {Props} from '@docusaurus/types';
import type {FasterConfig, Props} from '@docusaurus/types';
const CSS_REGEX = /\.css$/i;
const CSS_MODULE_REGEX = /\.module\.css$/i;
@ -57,10 +57,12 @@ export async function createBaseConfig({
props,
isServer,
minify,
faster,
}: {
props: Props;
isServer: boolean;
minify: boolean;
faster: FasterConfig;
}): Promise<Configuration> {
const {
outDir,
@ -172,7 +174,7 @@ export async function createBaseConfig({
// Only minimize client bundle in production because server bundle is only
// used for static site generation
minimize: minimizeEnabled,
minimizer: minimizeEnabled ? getMinimizer() : undefined,
minimizer: minimizeEnabled ? await getMinimizers({faster}) : undefined,
splitChunks: isServer
? false
: {

View file

@ -17,19 +17,26 @@ import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
import ForceTerminatePlugin from './plugins/ForceTerminatePlugin';
import {createStaticDirectoriesCopyPlugin} from './plugins/StaticDirectoriesCopyPlugin';
import type {Props} from '@docusaurus/types';
import type {FasterConfig, Props} from '@docusaurus/types';
import type {Configuration} from 'webpack';
async function createBaseClientConfig({
props,
hydrate,
minify,
faster,
}: {
props: Props;
hydrate: boolean;
minify: boolean;
faster: FasterConfig;
}): Promise<Configuration> {
const baseConfig = await createBaseConfig({props, isServer: false, minify});
const baseConfig = await createBaseConfig({
props,
isServer: false,
minify,
faster,
});
return merge(baseConfig, {
// Useless, disabled on purpose (errors on existing sites with no
@ -60,10 +67,12 @@ export async function createStartClientConfig({
props,
minify,
poll,
faster,
}: {
props: Props;
minify: boolean;
poll: number | boolean | undefined;
faster: FasterConfig;
}): Promise<{clientConfig: Configuration}> {
const {siteConfig, headTags, preBodyTags, postBodyTags} = props;
@ -72,6 +81,7 @@ export async function createStartClientConfig({
props,
minify,
hydrate: false,
faster,
}),
{
watchOptions: {
@ -105,10 +115,12 @@ export async function createStartClientConfig({
export async function createBuildClientConfig({
props,
minify,
faster,
bundleAnalyzer,
}: {
props: Props;
minify: boolean;
faster: FasterConfig;
bundleAnalyzer: boolean;
}): Promise<{config: Configuration; clientManifestPath: string}> {
// Apply user webpack config.
@ -125,7 +137,7 @@ export async function createBuildClientConfig({
);
const config: Configuration = merge(
await createBaseClientConfig({props, minify, hydrate}),
await createBaseClientConfig({props, minify, faster, hydrate}),
{
plugins: [
new ForceTerminatePlugin(),

View file

@ -7,8 +7,14 @@
import TerserPlugin from 'terser-webpack-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import {importSwcJsMinifierOptions} from '../faster';
import type {CustomOptions, CssNanoOptions} from 'css-minimizer-webpack-plugin';
import type {WebpackPluginInstance} from 'webpack';
import type {FasterConfig} from '@docusaurus/types';
export type MinimizersConfig = {
faster: Pick<FasterConfig, 'swcJsMinimizer'>;
};
// See https://github.com/webpack-contrib/terser-webpack-plugin#parallel
function getTerserParallel() {
@ -24,7 +30,16 @@ function getTerserParallel() {
return terserParallel;
}
function getJsMinifierPlugin() {
async function getJsMinimizer({faster}: MinimizersConfig) {
if (faster.swcJsMinimizer) {
const terserOptions = await importSwcJsMinifierOptions();
return new TerserPlugin({
parallel: getTerserParallel(),
minify: TerserPlugin.swcMinify,
terserOptions,
});
}
return new TerserPlugin({
parallel: getTerserParallel(),
terserOptions: {
@ -85,18 +100,19 @@ function getAdvancedCssMinifier() {
});
}
export function getMinimizer(): WebpackPluginInstance[] {
function getCssMinimizer(): WebpackPluginInstance {
// This is an historical env variable to opt-out of the advanced minifier
// Sometimes there's a bug in it and people are happy to disable it
const useSimpleCssMinifier = process.env.USE_SIMPLE_CSS_MINIFIER === 'true';
const minimizer: WebpackPluginInstance[] = [getJsMinifierPlugin()];
if (useSimpleCssMinifier) {
minimizer.push(new CssMinimizerPlugin());
return new CssMinimizerPlugin();
} else {
minimizer.push(getAdvancedCssMinifier());
return getAdvancedCssMinifier();
}
return minimizer;
}
export async function getMinimizers(
params: MinimizersConfig,
): Promise<WebpackPluginInstance[]> {
return Promise.all([getJsMinimizer(params), getCssMinimizer()]);
}

View file

@ -21,9 +21,8 @@ export default async function createServerConfig(params: {
const baseConfig = await createBaseConfig({
props,
isServer: true,
// Minification of server bundle reduces size but doubles bundle time :/
minify: false,
faster: props.siteConfig.future.experimental_faster,
});
const outputFilename = 'server.bundle.js';

View file

@ -13,7 +13,7 @@ import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import webpack, {type Configuration, type RuleSetRule} from 'webpack';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import {getSwcJsLoaderFactory} from '../faster';
import {importSwcJsLoaderFactory} from '../faster';
import type {ConfigureWebpackUtils, DocusaurusConfig} from '@docusaurus/types';
import type {TransformOptions} from '@babel/core';
@ -180,7 +180,7 @@ export async function createJsLoaderFactory({
return ({isServer}) => jsLoader(isServer);
}
if (siteConfig.future?.experimental_faster.swcJsLoader) {
return getSwcJsLoaderFactory();
return importSwcJsLoaderFactory();
}
if (jsLoader === 'babel') {
return BabelJsLoaderFactory;

View file

@ -199,6 +199,7 @@ export default {
future: {
experimental_faster: {
swcJsLoader: true,
swcJsMinimizer: true,
},
experimental_storage: {
type: 'localStorage',
@ -210,7 +211,8 @@ export default {
```
- `experimental_faster`: An object containing feature flags to make the Docusaurus build faster. This requires adding the `@docusaurus/faster` package to your site's dependencies. Use `true` as a shorthand to enable all flags.
- `swcJsLoader`: Use `true` to replace the default [Babel](https://babeljs.io/) JS loader by [SWC](https://swc.rs/) loader to speed up the bundling phase.
- `swcJsLoader`: Use `true` to replace the default [Babel](https://babeljs.io/) JS loader by the [SWC](https://swc.rs/) JS loader to speed up the bundling phase.
- `swcJsMinimizer`: Use `true` to replace the default [`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin) JS minimizer ([Terser](https://terser.org/)) by the [SWC JS minimizer](https://swc.rs/docs/configuration/minification) to speed up the bundling minification phase.
- `experimental_storage`: Site-wide browser storage options that theme authors should strive to respect.
- `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`.
- `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior).

View file

@ -2002,7 +2002,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9":
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
@ -16080,21 +16080,21 @@ tempy@^0.6.0:
type-fest "^0.16.0"
unique-string "^2.0.0"
terser-webpack-plugin@^5.3.7, terser-webpack-plugin@^5.3.9:
version "5.3.9"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.7, terser-webpack-plugin@^5.3.9:
version "5.3.10"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
dependencies:
"@jridgewell/trace-mapping" "^0.3.17"
"@jridgewell/trace-mapping" "^0.3.20"
jest-worker "^27.4.5"
schema-utils "^3.1.1"
serialize-javascript "^6.0.1"
terser "^5.16.8"
terser "^5.26.0"
terser@^5.0.0, terser@^5.10.0, terser@^5.15.1, terser@^5.16.8:
version "5.19.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.0.tgz#7b3137b01226bdd179978207b9c8148754a6da9c"
integrity sha512-JpcpGOQLOXm2jsomozdMDpd5f8ZHh1rR48OFgWUH3QsyZcfPgv2qDCYbcDEAYNd4OZRj2bWYKpwdll/udZCk/Q==
terser@^5.0.0, terser@^5.10.0, terser@^5.15.1, terser@^5.26.0:
version "5.31.6"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1"
integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"