参考:
如何通过性能优化,将包的体积压缩了62.7%
雅虎35条
20210526-webpack深入学习,搭建和优化react项目
本文只专注于性能优化的这个部分。
总体来说分为两个方面:第一是开发环境中主要优化打包速度,第二是线上环境中主要优化分包大小。
打包阶段
使用speed-measure-webpack-plugin
插件,显示打包过程中的每个步骤耗时大小。然后根据耗费的时长去优化。
比如下图:
安装cache-loader打包缓存
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
cacheLoader,
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
]
}
happypack .在打包的时候开启多线程打包
UglifyJsPlugin开启多线程优化时间,有两个方式,可以都试一下看看哪个效果好一点。
- 开启多线程
// 在minimizer属性内添加
// 自定义js优化配置,将会覆盖默认配置
new UglifyJsPlugin({
parallel: true, //使用多进程并行运行来提高构建速度
sourceMap: false,
uglifyOptions: {
warnings: false,
compress: {
unused: true,
drop_debugger: true,
drop_console: true,
},
output: {
comments: false // 去掉注释
}
}
})
- 使用:
webpack-parallel-uglify-plugin
上图可以看出UglifyJsPlugin这个包花费时间太长,可以使用相同效果的另一个包,开启多核同步压缩增加压缩的效率。
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
new ParallelUglifyPlugin({
uglifyJS: {
output: {
beautify: false, // 是否保留空格和制表符,设置为不保留
comments: false, // 是否保留代码中的注释,设置为不保留
},
compress: {
drop_console: true, // 是否删除代码中的console语句,设置为删除
collapse_vars: false, // 是否内嵌虽然已经定义了,但是只用到一次的变量,设置为不使用
reduce_vars: false, // 是否提取出现了多次但是没有定义成变量去引用的静态值,设置为不转换
},
warnings: false // 是否在删除没有用到的代码时输出警告信息,设置为不警告
},
}),
线上优化阶段
这一部分主要在于分离包(组件懒加载、路径懒加载、按需引入)、合并包(optimization.splitChunks)、压缩包(NGINX: gzip)
具体可以看以前的文章20210526-webpack深入学习,搭建和优化react项目
使用webpack-bundle-analyzer
插件,显示打包后的包大小、包组成,然后针对去处理。
将大包拆小:
异步引入,路由懒加载 分包 import('@/components/Index')
按需引入组件,看看是不是直接把一个依赖都引入进来了,比如lodash需要引入babel-plugin-lodash等。
优化代码和合并相同的包
看看analyzer显示的第三方包有没有重复引入、打包到不同包的情况,这样的话就比较浪费空间,最好将第三方包合并一下,这里就使用到 optimization.splitChunks
。
压缩代码、去除无用代码
引入Uglifyjs进行代码压缩(第一步时间优化已经做了) Uglifyjs还会进行Tree-shaking剔除无用代码。比如引入了第三方包却没有用上的情况也会删除。
在服务器上的优化
-
图片和其他文件进行无损压缩,并上传到cdn上。
推荐网站:https://tinyjpg.com -
nginx配置开通gzip ,从线上拉取的代码能压缩2/3
巨强。
Next项目的实际实践
一年半年前进行过一次next项目的webpack优化,那时候的优化方向主要是splitChunks分包的数据。
半年前的参考文章可看博客列表:结合Next项目实际认识webpack.splitChunks
Next版本:12.3.4(因为使用next-plugin-antd-less 只能支持到next12。。)
Node版本:16.20.2
推荐阅读:v12.3.X的Next文档
经过半年多的持续优化,在webpack这方面又有了新的优化目标:
- 发现treeShaking没有生效,引入但是没有使用的资源仍然打包进去了。
- console.log正式环境禁用。
- 同一个组件在不同的包里被反复打包,导致一个页面上被多次下载。
- 部署构建太慢,想要加快速度。
Treeshaking
结论:next使用terser-plugin做的压缩和摇树,本地构建不会开,但是在构建环境会使用,因此确实会把没有引用的组件shake出去,不会影响使用。
调研过程:
- 一开始从本地上看以为失效了,查看了很多terser有关的问题,列在如下:
- https://github.com/vercel/next.js/issues/20804
- https://github.com/vercel/next.js/issues/49742
- 推荐使用next13+可以解决这个问题,于是重新实验,但是本地情况下仍然没有shake出去。
- nvm安装(next13+必须要node18以上)教程:
- https://blog.csdn.net/Cavin80/article/details/132831839
- https://juejin.cn/post/7083026831263137800
- 按照方案二,增加terser的参数
passes:2
可以解决,但是next并没有开放terserOptions给我们,因此只能另辟蹊径,找了半天源码,找到了一个v12版本可行的options插入方法。注意这个为后续的drop_console方案提供了思路。(不过事实证明passes也没什么变化)
// next v12版本的实验特性,用于配置swc压缩属性
experimental: { swcMinifyDebugOptions: { compress: { passes: 2 } } },
-
modularizeImports。经过不懈寻找,找到了这么一个属性,可以用于内部组件的按需引入。
官方推荐按需引入来达到treeShaking效果:https://github.com/vercel/next.js/issues/45687
https://vercel.com/blog/how-we-optimized-package-imports-in-next-js
-
最后发现:原来是没有部署到线上,正式构建环境是可以treeshaking成功的。
drop console
线上环境我们并不想显示意外的console,因此删除console也是一个需求。
next提供的没有起作用。
compiler: {
removeConsole: {
exclude: ['error'],
},
},
根据上面对源码的研究,找到了插入terserOptions的方式,增加了如下代码,push到构建环境后就能正常生效了。
experimental: {
swcMinifyDebugOptions: { compress: { passes: 2, drop_console: true } },
},
PS.中间有一段自行重新引入了terser-webpack-plugin没有成功。
config.optimization.minimizer = [
new TerserPlugin({
parallel: true,
sourceMap: true,
terserOptions: {
compress: { drop_console: isProduction },
},
}),
];
部署构建太慢,想要加快速度。
-
引入速度衡量依赖speed-measure-webpack-plugin
使用参考:https://github.com/vercel/next.js/discussions/33678
config.plugins.push(smp);
-
分析:注意vscode setting原本默认的展示日志行数太少,可以通过 “terminal.integrated.scrollback”:1000000配置。
经过测试速度插件,可以看出耗时较久的是next\ no-loaders\ less-loader,前两个应该是next内部自行实现的(很无奈),那我们只能从less-loader下手了。
接下来是省流版:本项目使用next-plugin-antd-less已经不再维护,并且不支持多线程处理,因此费劲了九牛二虎之力仍然没有效果。作为一次探究过程放在下面。
打印出next的loader配置,经过对next-plugin-antd-less源码的研究,实际上对于css的处理next都是放在oneOf中的,我们要处理也要优先处理oneOf中的loader。
我考虑的方案是:
- 采用thread-loader添加多线程处理。
- 或者采用happyPack添加多线程处理。
const withConsole = (config) => {
// console.log('----resolveLoader----\n',config.resolveLoader)
// console.log('----rules----\n',config.module.rules)
// console.log('---oneOf----\n',console.log(config.module.rules.map((rule) => Array.isArray(rule.oneOf) && rule)))
return {
...config,
webpack(webpackConfig, nextConfig) {
// const _oneOf = [];
// webpackConfig.module.rules.forEach((rule) => {
// if (Array.isArray(rule)) {
// rule.forEach((loader) => {
// Array.isArray(loader.oneOf) && _oneOf.push(loader);
// });
// } else {
// Array.isArray(rule.oneOf) && _oneOf.push(rule);
// }
// });
// // console.log('---oneOf----\n');
// _oneOf.forEach(rule=>{
// rule.oneOf.forEach(_d=>{
// if(_d.test?.toString() === /\.module\.(scss|sass)$/.toString()){
// console.log(
// '---sass----\n',
// '🌈',
// _d
// )
// }
// console.log(_d)
// })
// })
const config = nextConfig;
console.log('---config----\n', config);
return webpackConfig;
},
};
};
{
test: /(antd\/.*?\/style|@ant-design).*(?<![.]js)$/,
use: 'null-loader'
},
{ issuerLayer: 'edge-asset', type: 'asset/source' },
{
dependency: 'url',
loader: 'next-middleware-asset-loader',
type: 'javascript/auto',
layer: 'edge-asset'
},
{
test: /\.wasm$/,
loader: 'next-middleware-wasm-loader',
type: 'javascript/auto',
resourceQuery: /module/i
},
{ test: /\.m?js/, resolve: { fullySpecified: false } },
{
test: /\.(js|cjs|mjs)$/,
issuerLayer: 'api',
parser: { url: true }
},
{
oneOf: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object]
]
},
{
test: /\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i,
loader: 'next-image-loader',
issuer: { not: /\.(css|scss|sass)$/ },
dependency: { not: [Array] },
options: { isServer: true, isDev: false, basePath: '', assetPrefix: '' }
},
{ oneOf: [ [Object], [Object] ] },
{
test: /\.+(js|jsx|mjs|ts|tsx)$/,
use: [ 'thread-loader', [Object] ],
include: [Function (anonymous)],
type: 'javascript/auto'
},
{
test: /\.(jpg|jpeg|png|svg|gif|ico|webp|jp2|avif)$/,
issuer: /\.\w+(?<!(s?c|sa)ss)$/i,
exclude: undefined,
use: [ [Object] ]
}
]
但是问题点在于,我打印出了所有的loader配置以后发现 找不到less-loader。。
然后我没有办法,把next-plugin-antd-less源码的插件引入了项目测试一下效果,在手动引入的代码文件中增加了thread-loader.
报错:UnhandledPromiseRejectionWarning: TypeError: loaderContext.getLogger is not a function
查找了一下这个问题是因为less-loader的版本问题导致的,降低了一下less-loader的版本。。。
https://github.com/webpack-contrib/thread-loader/issues/135
最后效果看起来确实少了5-6秒,但是不太值得。最终放弃了这个方案,以及happyPack也是同样的原因,做不到再把plugin引进源码文件再处理一次。