前言
我在公司实习的时候参与的一个 react + typescript 的项目,这个项目在自动化部署测试环境时很慢,每次都要 10 分钟左右。当时我抱着研究研究的心态,安装了 speed-measure-webpack-plugin 插件。测试发现 TerserPlugin、babel-loader 这哥俩耗时贼长,项目打包耗时6分钟左右。
esbuild
在调查研究优化方案的时候,发现了 esbuild , an extremely fast bundler for the web
在了解到有 esbuild-loader后,便萌生想用它替代 babel-loader 的念头。看看文档。
esbuild-loader
⚠️ esbuild is not stable yet and can have dramatic differences across releases. Using a different version of esbuild is not guaranteed to work.
翻译一下:esbuild 还不稳定,并且在不同版本之间可能存在巨大差异。不能保证可以正常工作。
嗯~,看来我们最好使用稳定版本的 esbuild。
esbuild-loader lets you harness the speed of esbuild in your Webpack build by offering faster alternatives for transpilation (eg. babel-loader/ts-loader) and minification (eg. Terser)!
翻译一下:esbuild-loader 通过提供更快的转译(例如 babel-loader/ts-loader)和缩小(例如 Terser)替代方案,让您在 Webpack 构建中利用 esbuild 的速度!
看来官方确实推荐使用 esbuild-loader 呢,除了替换 babel-loader,也提供了 ESBuildMinifyPlugin 替换 terser-plugin。
当然官方也有压缩 css 的方案。
craco + esbuild-loader 改写 rca
接下来就是实践环节,我们使用 craco 来改写 react-create-app 的 webpack 配置
craco 提供了 loaderByname、addAfterLoader、removeLoaders api 查找、增加、删除 Webpack Config 中的 loader,试一下
const {
loaderByName,
removeLoaders,
addAfterLoader,
addPlugins,
removePlugins,
pluginByName,
} = require('@craco/craco');
const { ESBuildMinifyPlugin } = require('esbuild-loader');
module.exports = {
webpack: {
configure: (webpackConfig, { env, paths }) => {
addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
test: /\.(js|ts|tsx|jsx)$/,
include: [paths.appSrc],
loader: require.resolve('esbuild-loader'),
options: {
loader: 'tsx',
target: 'es2016',
},
});
removeLoaders(webpackConfig, loaderByName('babel-loader'));
addPlugins(
webpackConfig,
new ESBuildMinifyPlugin({
target: 'es2016',
})
);
removePlugins(webpackConfig, pluginByName('TerserPlugin'));
return smp.wrap(webpackConfig);
},
}
};
build 一下,yarn craco build
啊这,报错了???
(node:41832) UnhandledPromiseRejectionWarning: TypeError: matcher is not a function
查了一下 craco 的 issue ,发现官方推荐字面量的写法来增加、删除插件。
plugins: {
add: [
new ESBuildMinifyPlugin({
target: 'es2016',
}),
],
remove: ['TerserPlugin'],
},
看看效果,相当不错:
(PS: 好奇怪,为什么没能把 TerserPlugin 删掉)
craco-esbuilt
查找相关资料的时候发现了有插件 craco-esbuild 实现了 rca 接入 esbuild-loader 这一过程。
craco-esbuild 利用了 craco 提供了 overrideWebpackConfig hook,将替换 babel-loader,TerserPlugin 的逻辑封装到插件中,同时增加了 options 的判断与获取,重写了增加、删除压缩的方法。放源码。
const fs = require('fs');
const { loaderByName, removeLoaders, addAfterLoader } = require('@craco/craco');
const { ESBuildMinifyPlugin } = require('esbuild-loader');
const removeMinimizer = (webpackConfig, name) => {
const idx = webpackConfig.optimization.minimizer.findIndex(
(m) => m.constructor.name === name
);
webpackConfig.optimization.minimizer.splice(idx, 1);
};
const replaceMinimizer = (webpackConfig, name, minimizer) => {
const idx = webpackConfig.optimization.minimizer.findIndex(
(m) => m.constructor.name === name
);
idx > -1 && webpackConfig.optimization.minimizer.splice(idx, 1, minimizer);
};
module.exports = {
/**
* To process the js/ts files we replace the babel-loader with the esbuild-loader
*/
overrideWebpackConfig: ({
webpackConfig,
pluginOptions,
context: { paths },
}) => {
const useTypeScript = fs.existsSync(paths.appTsConfig);
const esbuildLoaderOptions =
pluginOptions && pluginOptions.esbuildLoaderOptions;
// add includePaths custom option, for including files/components in other folders than src
// Used as in addition to paths.appSrc, optional parameter.
const optionalIncludes =
(pluginOptions && pluginOptions.includePaths) || [];
// add esbuild-loader
addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: [paths.appSrc, ...optionalIncludes],
loader: require.resolve('esbuild-loader'),
options: esbuildLoaderOptions
? esbuildLoaderOptions
: {
loader: useTypeScript ? 'tsx' : 'jsx',
target: 'es2015',
},
});
// remove the babel loaders
removeLoaders(webpackConfig, loaderByName('babel-loader'));
// Replace terser with esbuild
const minimizerOptions = (pluginOptions || {}).esbuildMinimizerOptions || {
target: 'es2015',
css: true,
};
replaceMinimizer(
webpackConfig,
'TerserPlugin',
new ESBuildMinifyPlugin(minimizerOptions)
);
// remove the css OptimizeCssAssetsWebpackPlugin
if (minimizerOptions.css) {
removeMinimizer(webpackConfig, 'OptimizeCssAssetsWebpackPlugin');
}
return webpackConfig;
},
/**
* To process the js/ts files we replace the babel-loader with the esbuild jest loader
*/
overrideJestConfig: ({ jestConfig, pluginOptions }) => {
if (pluginOptions && pluginOptions.skipEsbuildJest) return jestConfig;
const defaultEsbuildJestOptions = {
loaders: {
'.js': 'jsx',
'.test.js': 'jsx',
'.ts': 'tsx',
'.test.ts': 'tsx',
},
};
const esbuildJestOptions =
(pluginOptions && pluginOptions.esbuildJestOptions) ||
defaultEsbuildJestOptions;
// Replace babel transform with esbuild
// babelTransform is first transformer key
/*
transform:
{
'^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': 'node_modules\\react-scripts\\config\\jest\\babelTransform.js',
'^.+\\.css$': 'node_modules\\react-scripts\\config\\jest\\cssTransform.js',
'^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': 'node_modules\\react-scripts\\config\\jest\\fileTransform.js'
}
*/
const babelKey = Object.keys(jestConfig.transform)[0];
// We replace babelTransform and add loaders to esbuild-jest
jestConfig.transform[babelKey] = [
require.resolve('esbuild-jest'),
esbuildJestOptions,
];
// Adds loader to all other transform options (2 in this case: cssTransform and fileTransform)
// Reason for this is esbuild-jest plugin. It considers only loaders or other options from the last transformer
// You can see it for yourself in: /node_modules/esbuild-jest/esbuid-jest.js:21 getOptions method
// also in process method line 32 gives empty loaders, because options is already empty object
// Issue reported here: https://github.com/aelbore/esbuild-jest/issues/18
Object.keys(jestConfig.transform).forEach((key) => {
if (babelKey === key) return; // ebuild-jest transform, already has loader
// Checks if value is array, usually it's not
// Our example is above on 70-72 lines. Usually default is: {"\\.[jt]sx?$": "babel-jest"}
// (https://jestjs.io/docs/en/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object)
// But we have to cover all the cases
if (
Array.isArray(jestConfig.transform[key]) &&
jestConfig.transform[key].length === 1
) {
jestConfig.transform[key].push(esbuildJestOptions);
} else {
jestConfig.transform[key] = [
jestConfig.transform[key],
esbuildJestOptions,
];
}
});
return jestConfig;
},
};
欢迎大家在评论区一起讨论 rca 的优化方案呀