webpack性能优化篇(建议收藏)
本文将和大家一起分享一下如何从webpack
上来做出构建的优化。
为什么要做 webpack 配置的优化?
最近刚做了一个保险业务的大型项目中,项目页面达到800+
,随之带来的挑战就是项目中打包出来的 js 体积越来越大,构建速度越来越缓慢,无疑,从webpack
构建配置上就需要作出一系列的优化了。以下配置的优化,均在真实项目中有过实战,希望对大家有帮助。
如何作出具体的优化?
声明:本文基于webpack
版本号如下:
"webpack": "^4.42.0", "webpack-cli": "^3.3.11"
在作出webpack
配置优化之前,首先我们需要借助一些webpack 插件
来分析我们当前的构建日志
,以及构建速度
、构建体积
等。
初级分析:使用 webpack 内置的 stats
通过设置stats
来统计我们的构建的信息。
我们在package.json
中添加如下配置:
"scripts": { "build:stats": "webpack --config build/webpack.config.prod.js --json > stats.json" }
运行npm run build:stats
后,再执行npm run prod
后,在我们项目的根目录下会生成一个stats.json
文件,这个文件会记录我们项目构建的各种信息,同时也可以stats
后看到控制台打印出对应的构建信息。
速度分析:使用 speed-measure-webpack-plugin
刚才提到的stats
来分析构建日志,但是stats
的分析还是比较有限,如果我们想知道我们使用的哪个lodaer
,或者是哪个plugin
的具体耗时该怎么办呢?speed-measure-webpack-plugin
就是一个不错的分析插件。
安装
npm i speed-measure-webpack-plugin -D
配置
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin'); const smp = new SpeedMeasureWebpackPlugin(); module.exports = smp.wrap(WebpackMerge(WebpackConfig, { mode: "production", devtool: "hidden-source-map", entry: { app: resolve(__dirname, "../src/main") } }));
运行npm run prod
,可以很清楚的知道我们每一个loader
以及plugin
运行的耗时以及我们的总打包的耗时。
分析体积:webpack-bundle-analyzer
安装
npm i webpack-bundle-analyze -D
配置
const WebpackBundleAnalyzer = require('webpack-bundle-analyzer'); const { ANALYZE } = process.env; const { BundleAnalyzerPlugin } = WebpackBundleAnalyzer; if (ANALYZE === 'true') { PluginConfig.push(new BundleAnalyzerPlugin()); }
我们在package.json
中添加如下配置
"analyz": "cross-env NODE_ENV=production ANALYZE=true npm_config_report=true npm run prod",
运行npm run analyz
,浏览器会自动打开http://127.0.0.1:8888/
,此时我们就可以很清晰的看到每一个打包后的 js 文件体积Gzip
前跟Gzip
后的大小对比,以及一些基础包体积大小的对比。
上述,我们借助了一些插件来帮助我们分析项目中打包的体积、耗时等,那接下来我们就要从构建的速度上来进行进一步的分析并优化。
多进程/多实例构建:资源并行解析可选方案
使用 HappyPack 解析资源
原理:每次 webapck 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中。
安装
npm i happypack -D
配置
const HappyPack = require('happypack'); plugins: [ new HappyPack({ // id 标识符,要和 rules 中指定的 id 对应起来 id: 'babel', // 需要使用的 loader,用法和 rules 中 Loader 配置一样 // 可以直接是字符串,也可以是对象形式 loaders: ['babel-loader'] }) ],
运行npm run prod
后对比可见,构建的时间缩短了2 秒钟
。
并行压缩 terser-webpack-plugin
使用 terser-webpack-plugin
插件
安装
npm i terser-webpack-plugin@1.3.0 -D
配置
const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimizer: [ new TerserPlugin({ parallel: true, cache: true }) ] } }
运行npm run prod
后对比可见,构建的时间缩短了500ms
。
分包:设置 Externals
思路:将 vue
、vue-router
基础包通过 cdn
引入,不打入 bundle
中
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title></title> </head> <body> <div id="app"> <router-view></router-view> </div> <!-- 正常的引入 cdn 资源即可 --> <script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script> <script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script> </body> </html>
配置
module.exports = { module: { ... }, externals: { 'vue': 'Vue', 'vue-router': 'VueRouter' } }
如果在项目中继续使用的话,我们依然可以使用import
的方式引入。
import Vue from 'vue' import VueRouter from 'vue-router'
这样配置的话 webpack
在 dev
运行或 build
打包时,就不会去本地组件包中查找这些在 externals
中注册的组件了(自然也不会将他们打包到一个 app.js
中去),而是会去 window
域下直接调用 Vue
, VueRouter
等对象。
进一步分包:预编译资源模块 DLLPlugin
思路:将 vue、vue-router 等 基础包打包成一个文件。
方法:使用 webapck 内置的插件 DLLPlugin 进行分包,DllReferencePlugin 对 manifest.json 引用。
配置
build
目录下新建webpack.config.dll.js
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { library: [ 'vue', 'vue-router' ] }, output: { filename: '[name]_[hash].dll.js', path: path.join(__dirname, '../library'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]_[hash]', path: path.join(__dirname, '../library/[name].json') }) ] }
我们在package.json
中添加如下配置:
"dll": "webpack --config build/webpack.config.dll"
运行npm run dll
后,项目根目录下会自动生成一个library
文件夹,其中library.json
文件就是我们接下来要在webpack.config.prod.js
中进行的映射。
webpack.config.prod.js
配置:
const Webpack = require('webpack'); module.exports = { plugins: [ new Webpack.DllReferencePlugin({ manifest: require('../library/library.json') }) ], }
再次执行npm run prod
后对比发现,分包后的 app.js 体积比分包前 app.js 体积小了30kb
,构建速度上也有微弱的减少,当然我们这里只是把vue
、vue-router
抽离了出来做个演示,那当我们项目比较大的时候,可以把更多的业务基础包抽离出来,效果会更加明显。
开启缓存
babel-loader
开启缓存,在babel-loader
后边加上参数cacheDirectory=true
配置
plugins: [ ...BasePlugins, new HappyPack({ // id 标识符,要和 rules 中指定的 id 对应起来 id: 'babel', // 需要使用的 loader,用法和 rules 中 Loader 配置一样 // 可以直接是字符串,也可以是对象形式 loaders: ['babel-loader?cacheDirectory=true'] }), ]
执行npm run prod
后,对比发现缓存开启后比开启前快了600ms
使用 cache-loader
或者 hard-source-webpack-plugin
安装
npm i hard-source-webpack-plugin -D
配置
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { plugins: [ new HardSourceWebpackPlugin() ], }
执行npm run prod
后,会发现在我们的node_modules
目录下会自动帮助我们生成一个.cache
目录,里边存放的就是每次构建缓存的文件,运行后对比发现缓存开启后比开启前快了1800ms
,时间大大缩短,当然我们这里也只是为了演示,缩短的时间不是很明显,一旦在项目体积大的时候,开启缓存构建,速度会有巨大的提升。
缩小构建目标
目的:尽可能的少构建模块,比如 babel-loader
不解析 node_modules
配置
module.exports = { module: { { test: /\.js$/, use: [ { loader: 'thread-loader', options: { workers: 3 } }, "babel-loader", ], exclude: /node_modules/ } } }
减少文件搜索范围
- 优化
resolve.modules
配置(减少模块搜索层级) - 优化
resolve.extensions
配置 - 合理使用
alias
配置
module.exports = { resolve: { extensions: [".js", ".json", ".css", ".less", ".vue"], alias: { vue$: "vue/dist/vue.common.js", "@": resolve(__dirname, "../src") } } }
tree shaking
概念:1 个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到 bundle 里面去,tree shaking 就是只把用到的方法打入 bundle ,没用到的方法会在 uglify 阶段被擦除掉。
使用
webpack4
中我们把mode
设置为production
情况下默认开启tree-shaking
那js
的tree-shaking
这里就不再细描述了,有兴趣的小伙伴们可以自己动手试试,那关于css
的tree-shaking
我们该如何进行配置呢?
在没有进行开启css
的tree-shaking
前,我们先来测试一下,在index.vue
中写一行没有使用的css
,看一下会不会被打包进去。
执行npm run prod
后发现,确实被打包到 js 文件中了。
使用purgecss-webpack-plugin
,前提是需要配置mini-css-extract-plugin
配合使用开启css
的tree-shaking
。
安装
npm i mini-css-extract-plugin purgecss-webpack-plugin -D
配置
const Path = require("path"); const glob = require('glob'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const PurgecssPlugin = require('purgecss-webpack-plugin'); const PATHS = { src: path.join(__dirname, 'src') }; module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: '[name]_[contenthash:8].css' }), // 开启 css 的 tree-shaking new PurgecssPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), }) ] }
运行完npm run prod
后发现,在index.vue
中写一行没有使用到的unused-css
这个样式被擦出掉了,没有被打包进去。
图片压缩
通常一个项目我们会引入很多各种格式的图片,多张图片被打包以后,如果不做压缩的话,体积还是相当大的,所以生产环境对图片体积的压缩就显得格外重要了。
方式
- 使用
tinypng
手动压缩,比较零碎,也不够自动化 - imagemin
image-webpack-loader
来进行自动压缩
这里我们就采用image-webpack-loader
来实现对图片的自动压缩。
安装
npm i image-webpack-loader -D
配置
module.exports = { module: { { test: /\.(jpg|jpeg|png|gif)$/, use: [ { loader: 'url-loader', options: { limit: 8192, outputPath: "img/", name: "[name]-[hash:6].[ext]" } }, { loader: 'image-webpack-loader', options: { mozjpeg: { progressive: true, quality: 65 }, // optipng.enabled: false will disable optipng optipng: { enabled: false, }, pngquant: { quality: '65-90', speed: 4 }, gifsicle: { interlaced: false, }, // the webp option will enable WEBP webp: { quality: 75 } } } ] } } }
<template> <div class="container"> {{ msg }} <img :src="require('@/images/bg.jpg')"> </div> </template>
运行npm run prod
后对比发现,压缩后的图片的体积大大缩小。
压缩前:
压缩后:
构建体积优化:动态 Polyfill
通常我们在项目中会使用 babel 来将很多 es6 中的 API 进行转换成 es5,但是还是有很多新特性没法进行完全转换,比如 promise、async await、map、set 等语法,那么我们就需要通过额外的 polyfill(垫片)来实现语法编译上的支持。
方案 | 优点 | 缺点 |
---|---|---|
babel-polyfill | vue、react 官方支持 | 包的体积比较大,很难单独抽离 async await、map、set 等语法 |
babel-plugin-transform-runtime | 只对需要使用到 async/await 时,才会自动引入 polyfill,减小库与工具包的体积 | 不能 polyfill 原型上的一些方法 |
polyfill-service | 只返回用户需要用到的 polyfill,而且由社区来维护,比如polyfill | 部分浏览可能不能识别 |
这里我们还是推荐使用第三种方式,由polyfill
官方为我们提供的服务。
我们可以先来使用polyfill
验证一下,在不同的User Agent
,是不是会下发不同的polyfill
。
iphone5
iphone6/7/8
iphoneX
我们对比可以发现,不同的手机机型,我们去访问 polyfill.io/v3/polyfill.min.js 的时候,资源的体积大小是不一样的。
项目中使用
<script src='https://polyfill.io/v3/polyfill.min.js'></script>
总结
- 虽然,
webpack5
已经在 2020 年的 10 月 10 号完成了发布,但是目前基于项目架构在生产环境下的稳定性、可维护性来讲,我们这里依然采用的是 webpack4 来分析构建的优化策略。 - 当然,
webpack5
在项目打包优化上会更具有优势,如持久化的缓存、对node
中polyfill
的移除、更优的tree-shaking
、以及令人兴奋的Module Federation
,这些新特性还是很值得大家去升级探索的。感兴趣的小伙伴可以看我另一篇文章中给大家分享的webpack5 项目升级实战。
码云笔记 » webpack性能优化篇(建议收藏)