【前端优化】使用speed-measure-webpack-plugin分析前端运行/打包耗时,使用esbuild-loader优化vue项目
记录下
项目安装speed-measure-webpack-plugin
打包分析:
修改vue.config.js文件
speed-measure-webpack-plugin进行构建速度分析,需要包裹整个 configureWebpack 配置
const originalConfig = { publicPath: \'/\', assetsDir: \'assets\', parallel: true, devServer: { hot: true }, lintOnSave: false, runtimeCompiler: true, chainWebpack: (config) => { config.resolve.alias .set(\'@\', resolve(\'src\')) .set(\'@views\', resolve(\'src/views\')) .set(\'@comp\', resolve(\'src/components\')) .set(\'@root\', resolve(\'/\')) config.module .rule(\'images\') .test(/\\.(png|jpe?g|gif|svg)(\\?.*)?$/) .type(\'asset\') .parser({ dataUrlCondition: { maxSize: 8 * 1024 // 8KB以下转base64 //2560 } }) // .set(\'generator\', { // filename: \'img/[name].[hash:8][ext]\' // }) config.module .rule(\'images\') .use(\'image-webpack-loader\') .loader(\'image-webpack-loader\') .options({ bypassOnDebug: true }) .end() }, configureWebpack: { cache: { type: \'filesystem\', // 使用文件缓存 buildDependencies: { config: [__filename] // 缓存依赖 }, allowCollectingMemory: true, maxMemoryGenerations: 1 }, optimization: { minimizer: minimizer, removeEmptyChunks: process.env.NODE_ENV === \'production\', splitChunks: splitChunks }, plugins: plugins, module: { noParse: /jquery/, rules: [ { test: /\\.js$/, include: path.resolve(__dirname, \'src\'), exclude: /node_modules/, use: [ { loader: \'thread-loader\', options: { workers: cpuCount, workerParallelJobs: 20, workerNodeArgs: [\'--max-old-space-size=1024\'] // 限制子进程内存 // poolTimeout: 2000 // 空闲时自动关闭 } }, { loader: \'babel-loader\', options: { babelrc: true, cacheDirectory: true } } ] } ] } }, css: { loaderOptions: { // You need to configure a global variable if you want to use it in a component scss: { additionalData: \'@import \"~@/assets/css/variables.scss\";\' } } }, // 设为false打包时不生成.map文件 productionSourceMap: false}module.exports = { ...originalConfig, configureWebpack: smp.wrap(originalConfig.configureWebpack)}
npm run dev运行,获取分析:
SMP ⏱General output time took 7 mins, 42.001 secs SMP ⏱ PluginsDefinePlugin took 0 secsTerserPlugin took 0 secs SMP ⏱ Loaders@vue/vue-loader-v15, andmini-css-extract-plugin, andcss-loader, andpostcss-loader, andsass-loader, and@vue/vue-loader-v15 took 4 mins, 58.75 secs module count = 2340css-loader, and@vue/vue-loader-v15, andpostcss-loader, andsass-loader, and@vue/vue-loader-v15 took 4 mins, 53.93 secs module count = 1170@vue/vue-loader-v15, andthread-loader, andbabel-loader, andthread-loader, andbabel-loader, and@vue/vue-loader-v15 took 3 mins, 22.91 secs module count = 3430mini-css-extract-plugin, andcss-loader, andpostcss-loader took 2 mins, 41.67 secs module count = 21thread-loader, andbabel-loader, andthread-loader, andbabel-loader took 2 mins, 33.61 secs module count = 404mini-css-extract-plugin, andcss-loader, andpostcss-loader, andsass-loader took 2 mins, 12.24 secs module count = 4modules with no loaders took 2 mins, 2.27 secs module count = 2244image-webpack-loader took 1 min, 36.096 secs module count = 327@vue/vue-loader-v15 took 1 min, 31.51 secs module count = 5127css-loader, andpostcss-loader took 1 min, 13.14 secs module count = 21css-loader, andpostcss-loader, andsass-loader took 50.62 secs module count = 4@vue/vue-loader-v15, andmini-css-extract-plugin, andcss-loader, andpostcss-loader, and@vue/vue-loader-v15 took 49.91 secs module count = 6css-loader, and@vue/vue-loader-v15, andpostcss-loader, and@vue/vue-loader-v15 took 15.11 secs module count = 3html-webpack-plugin took 1.9 secs module count = 1raw-loader took 0.819 secs module count = 1script-loader took 0.032 secs module count = 1
修改配置
1.图片处理仅在生产环境使用
if (process.env.NODE_ENV === \'production\') { config.module .rule(\'images\') .test(/\\.(png|jpe?g|gif|svg)(\\?.*)?$/) .type(\'asset\') .parser({ dataUrlCondition: { maxSize: 8 * 1024 // 8KB以下转base64 //2560 } }) // .set(\'generator\', { // filename: \'img/[name].[hash:8][ext]\' // }) config.module .rule(\'images\') .use(\'image-webpack-loader\') .loader(\'image-webpack-loader\') .options({ bypassOnDebug: true }) .end() } else { config.module.rule(\'images\').uses.delete(\'image-webpack-loader\') // 移除图像压缩 }
- scss设置
css: { loaderOptions: { // You need to configure a global variable if you want to use it in a component scss: { implementation: require(\'sass\'), additionalData: \'@import \"~@/assets/css/variables.scss\";\' } } },
- 改用esbuild-loader,用了之后是要快一点,但这个速度感觉还是不对啊
// 删除原有的babel-loader配置 config.module.rules.delete(\'js\') // 添加esbuild-loader config.module .rule(\'js\') .test(/\\.(js|mjs|jsx)$/) // .exclude.add(/node_modules/) // .end() .use(\'esbuild-loader\') .loader(\'esbuild-loader\') .options({ target: \'es2015\', loader: \'jsx\' })
thread-loader, and babel-loader took 1 min, 23.34 secs module count = 374-------esbuild-loader took 52.51 secs module count = 395
再次优化后,打包速度提升:
SMP ⏱ General output time took 4 mins, 5.18 secs SMP ⏱ PluginsDefinePlugin took 0.002 secs SMP ⏱ Loaderscss-loader, and @vue/vue-loader-v15, and postcss-loader, and sass-loader, and @vue/vue-loader-v15 took 3 mins, 16.68 secs module count = 1170@vue/vue-loader-v15 took 2 mins, 11.74 secs module count = 5127@vue/vue-loader-v15, and esbuild-loader, and @vue/vue-loader-v15 took 1 min, 50.85 secs module count = 3430modules with no loaders took 1 min, 27.56 secs module count = 2609css-loader, and postcss-loader took 1 min, 11.28 secs module count = 21css-loader, and postcss-loader, and sass-loader took 1 min, 8.42 secs module count = 4esbuild-loader took 52.51 secs module count = 395css-loader, and @vue/vue-loader-v15, and postcss-loader, and @vue/vue-loader-v15 took 8.85 secs module count = 3@vue/vue-loader-v15, and vue-style-loader, and css-loader, and postcss-loader, and sass-loader, and @vue/vue-loader-v15 took 2.93 secs module count = 2340vue-style-loader, and css-loader, and postcss-loader took 0.373 secs module count = 21vue-style-loader, and css-loader, and postcss-loader, and sass-loader took 0.197 secs module count = 4html-webpack-plugin took 0.141 secs module count = 1@vue/vue-loader-v15, and vue-style-loader, and css-loader, and postcss-loader, and @vue/vue-loader-v15 took 0.016 secs module count = 6
但CSS相关加载器耗时还是非常长,没搜到怎么优化。。。。
webpack5新增了一个懒加载属性 lazyCompilation,还可以使用 DllPlugin 进行分包,都能提升运行速度;后面看看
继续尝试优化
使用const { EsbuildPlugin } = require(‘esbuild-loader’)替代const TerserPlugin = require(‘terser-webpack-plugin’)
const { EsbuildPlugin } = require(\'esbuild-loader\')const terserUglifyPlugin = new EsbuildPlugin({ target: \'es2015\', css: true, // Apply minification to CSS assets minify: true})// new TerserPlugin({// parallel: cpuCount, // 限制并行数// exclude: [\'/node_modules/\'],// terserOptions: {// parse: {},// compress: {// warnings: false,// drop_console: true,// drop_debugger: true// },// output: {// comments: true,// beautify: false// },// warnings: false// }// })
结果:有提升,但不太明显
当前完整vue.config.js文件:
const path = require(\'path\')const fs = require(\'fs\')const webpack = require(\'webpack\')const TerserPlugin = require(\'terser-webpack-plugin\')// const BundleAnalyzerPlugin = require(\'webpack-bundle-analyzer\').BundleAnalyzerPlugin// const SpeedMeasurePlugin = require(\'speed-measure-webpack-plugin\')// const smp = new SpeedMeasurePlugin()const { EsbuildPlugin } = require(\'esbuild-loader\')const os = require(\'os\')const execSync = require(\'child_process\').execSyncconst dayjs = require(\'dayjs\')const resolve = (dir) => { return path.join(__dirname, dir)}// 获取CPU核心数(限制最大线程数)const cpuCount = Math.max(4, os.cpus().length - 1) // 最小4个线程// 如果自定义时间则优先设定时间,如果没有则使用当前时间const buildTime = process.env.BUILD || dayjs().format(\'YYYY/MM/DD hh:mm:ss\')let splitChunks = falseif (process.env.NODE_ENV === \'production\') { const commitId = execSync(\'git log | head -n 1 | cut -c 8-\', { encoding: \'utf8\' }) console.log(\'编译时间\', buildTime) // 因为扫描漏洞问题,需要在打包的时候删除ip地址 fs.writeFile(resolve(\'config/devApiUrl.js\'), \"module.exports = \'\'\\r\\n\", function (err) { if (err) { console.error(\'IP删除失败\', err) } else { console.log(\'IP删除成功\') } }) fs.writeFile(resolve(\'public/.versionLog\'), `id:${commitId}\\ntime:${dayjs().format(\'YYYY/MM/DD hh:mm:ss\')}`, function (err) { if (err) { console.error(\'\\ncommitID写入失败\', err) } else { console.log(\'\\ncommitID写入成功\') } }) splitChunks = { maxInitialRequests: 10, // 默认是 30 maxAsyncRequests: 15, cacheGroups: { elementUI: { priority: 20, name: \'chunk-elementUI\', test: /element-ui/, chunks: \'all\' }, echart: { name: \'chunk-echart\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_echart/ }, xlsx: { name: \'chunk-xlsx\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_xlsx/ }, D3: { name: \'chunk-D3\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_d3/ }, jsPdf: { name: \'chunk-jsPdf\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_jspdf/ }, antvG6: { name: \'chunk-antvG6\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_@antv_g6/ }, GGEditor: { name: \'chunk-GGEditor\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_gg-editor-core/ }, xterm: { name: \'chunk-xterm\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_xterm/ }, html2canvas: { name: \'chunk-html2canvas\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_html2canvas/ }, zrender: { name: \'chunk-zrender\', // 单独 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\\\\/]node_modules[\\\\/]_zrender/ } } }}const esbuildloaderPlugin = new EsbuildPlugin({ target: \'es2015\', css: true, // Apply minification to CSS assets minify: true})const terserUglifyPlugin = new TerserPlugin({ parallel: cpuCount, // 限制并行数 exclude: [\'/node_modules/\'], terserOptions: { parse: {}, compress: { warnings: false, drop_console: true, drop_debugger: true }, output: { comments: true, beautify: false }, warnings: false }})// const threadLoader = require(\'thread-loader\')// threadLoader.warmup(// {// workers: cpuCount// },// [\'babel-loader\']// )const minimizer = process.env.NODE_ENV === \'production\' ? [terserUglifyPlugin] : [esbuildloaderPlugin]const plugins = [ new webpack.DefinePlugin({ \'process.env.BUILD_ENV\': JSON.stringify(process.env.NODE_ENV), \'process.env.BUILD_TIME\': JSON.stringify(buildTime) }) // new BundleAnalyzerPlugin()]module.exports = { publicPath: \'/\', assetsDir: \'assets\', parallel: true, devServer: { hot: true }, lintOnSave: false, runtimeCompiler: true, chainWebpack: (config) => { config.resolve.alias .set(\'@\', resolve(\'src\')) .set(\'@views\', resolve(\'src/views\')) .set(\'@comp\', resolve(\'src/components\')) .set(\'@root\', resolve(\'/\')) // .set(\'lodash\', path.resolve(process.cwd(), \'node_modules/lodash\')) // .set(\'xlsx\', path.resolve(process.cwd(), \'node_modules/xlsx\')) // config.module // .rule(\'scss\') // .use(\'thread-loader\') // .loader(\'thread-loader\') // .options({ // workers: cpuCount, // workerParallelJobs: 50, // poolTimeout: 2000 // }) // .before(\'sass-loader\') // 删除原有的babel-loader配置 config.module.rules.delete(\'js\') // 添加esbuild-loader config.module .rule(\'js\') .test(/\\.(js|mjs|jsx)$/) .exclude.add(/node_modules/) .end() .use(\'esbuild-loader\') .loader(\'esbuild-loader\') .options({ target: \'es2015\', loader: \'jsx\' }) if (process.env.NODE_ENV === \'production\') { config.module .rule(\'images\') .test(/\\.(png|jpe?g|gif|svg)(\\?.*)?$/) .type(\'asset\') .parser({ dataUrlCondition: { maxSize: 8 * 1024 // 8KB以下转base64 //2560 } }) // .set(\'generator\', { // filename: \'img/[name].[hash:8][ext]\' // }) config.module .rule(\'images\') .use(\'image-webpack-loader\') .loader(\'image-webpack-loader\') .options({ bypassOnDebug: true }) .end() } else { config.module.rule(\'images\').uses.delete(\'image-webpack-loader\') // 移除图像压缩 } }, configureWebpack: { cache: { type: \'filesystem\', // 使用文件缓存 buildDependencies: { config: [__filename] // 缓存依赖 }, allowCollectingMemory: true, maxMemoryGenerations: 1 }, optimization: { runtimeChunk: \'single\', minimizer: minimizer, removeEmptyChunks: process.env.NODE_ENV === \'production\', splitChunks: splitChunks }, plugins: plugins, module: { noParse: /jquery/, rules: [ // { // test: /\\.(js|mjs|jsx)$/, // loader: \'esbuild-loader\', // options: { // loader: \'jsx\', // target: \'es2015\' // // jsx: \'automatic\' // }, // exclude: [/node_modules/] // } // { // test: /\\.js$/, // include: path.resolve(__dirname, \'src\'), // exclude: /node_modules/, // use: [ // { // loader: \'thread-loader\', // options: { // workers: cpuCount // // workerParallelJobs: 20, // // workerNodeArgs: [\'--max-old-space-size=1024\'], // 限制子进程内存 // // poolTimeout: 2000 // 空闲时自动关闭 // } // }, // { // loader: \'babel-loader\', // options: { // babelrc: true, // cacheDirectory: true // } // } // ] // } ] } }, css: { loaderOptions: { scss: { implementation: require(\'sass\') // 不用每个文件引入了 // additionalData: \'@import \"~@/assets/css/variables.scss\";\' } } }, // 设为false打包时不生成.map文件 productionSourceMap: false}// module.exports = {// ...originalConfig,// configureWebpack: smp.wrap(originalConfig.configureWebpack)// }
其他
发现一个项目挺有意思的:https://github.com/privatenumber/minification-benchmarks
它比较了各种JavaScript压缩器的性能情况,值得参考
另外的参考文档:
https://segmentfault.com/a/1190000041455457
https://juejin.cn/post/7310102466569699366
https://yk2012.github.io/sgg_webpack5/senior/optimizePerformance.html#code-split
https://yk2012.github.io/sgg_webpack5/senior/optimizePerformance.html#code-split
https://zhuanlan.zhihu.com/p/708899387
爆肝总结万字长文笔记webpack5打包资源优化:
https://cloud.tencent.com/developer/article/2059818
https://webpack.docschina.org/concepts/
【万字总结】前端全方位性能优化指南(完结篇)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
https://blog.csdn.net/yong_su/article/details/146982304?spm=1001.2014.3001.5502