当我的项目越来越简单时,会面临着构建速度慢和构建进去的文件体积大的问题。webapck 构建优化对于大我的项目是必须要思考的一件事,上面咱们就从速度和体积两方面来探讨构建优化的策略。
剖析工具
在优化之前,咱们须要理解一些量化剖析的工具,应用它们来帮忙咱们剖析须要优化的点。
webpackbar
webpackbar 能够在打包时实时显示打包进度。配置也很简略,在 plugins 数组中退出即可:
const WebpackBar = require('webpackbar')
module.exports = {plugins: [ ... new WebpackBar() ]
}
speed-measure-webpack-plugin
应用 speed-measure-webpack-plugin
能够看到每个 loader 和 plugin 的耗时状况。
和一般插件的应用略有不同,须要用它的 wrap 办法包裹整个 webpack 配置项。
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap({
entry: './src/main.js',
...
})
打包后,在命令行的输入信息如下,咱们能够看出哪些 loader 和 plugin 耗时比拟久,而后对其进行优化。
webpack-bundle-analyzer
webpack-bundle-analyzer
以可视化的形式让咱们直观地看到打包的 bundle 中到底蕴含哪些模块内容,以及每一个模块的体积大小。咱们能够依据这些信息去剖析我的项目构造,调整打包配置,进行优化。
在 plugins 数组中退出该插件。构建实现后,默认会在 http://127.0.0.1:8888/
展现剖析后果。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {plugins: [ ... new BundleAnalyzerPlugin() ]
}
webpack-bundle-analyzer
会计算出模块文件在三种情景下的大小:
- stat:文件未通过任何转换的原始大小
- parsed:文件通过转换后的输入大小(比方 babel-loader 转换 ES6->ES5、UglifyJsPlugin 压缩等等)
- gzip:parsed 后的文件,通过 Gzip 压缩的大小
应用speed-measure-webpack-plugin
和webpack-bundle-analyzer
自身也会减少打包工夫(webpack-bundle-analyzer
特地耗时),所以倡议这两个插件在开发剖析时应用,而在生产环境去掉。
优化构建速度
多过程构建
运行在 Node.js 之上的 Webpack 是单线程的,就算有多个工作同时存在,它们也只能一个一个排队执行。当我的项目比较复杂时,构建就会比较慢。现在大多数 CPU 都是多核的,咱们能够借助一些工具,充沛开释 CPU 在多核并发方面的劣势。
比拟常见的有happypack
、thread-loader
。
happypack
happypack 可能将构建工作合成给多个子过程去并发执行,子过程解决完后再把后果发送给主过程。应用配置如下,就是把原有的 loader 的配置转移到 happyPack 中去解决。
const Happypack = require('happypack')
module.exports = {
module:{
rules:[
{
test: /\.js$/,
use: 'happypack/loader?id=babel' // 问号前面的查问参数指定了解决这类文件的 HappyPack 实例的名字
},
]
},
plugins:[
new Happypack({
id: 'babel', //HappyPack 实例名,对应下面 rules 中的“id=babel”use: ['babel-loader'] // 本来要应用的 loader
})
]
}
thread-loader
happypack 的作者曾经没有这个我的项目进行保护了,在 webpack4 之后,能够应用thread-loader
。
thread-loader
应用起来很简略,就是把它搁置在其它 loader 之前,如下所示。搁置在这个 thread-loader
之后的 loaders 会运行在一个独自的 worker 池中。
module.exports = {
module:{
rules:[
{
test: /\.js$/,
use: ['thread-loader','babel-loader']
}
]
},
}
参考 webpack 视频解说:进入学习
如果是小我的项目,不倡议开启多过程构建,因为开启过程是须要破费工夫的,构建速度反而会变慢。
利用缓存
利用缓存能够晋升二次构建速度(上面的比照图都是二次构建的速度)。应用缓存后,在 node_modules 中会有一个 .cache
目录,用于寄存缓存的内容。
cache-loader
在一些性能开销较大的 loader 之前增加此cache-loader
,以将后果缓存到磁盘中。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader','babel-loader']
}
]
}
}
能够看出,应用 cache-loader
后,构建速度有非常明显的晋升。
对 babel-loader
应用缓存,也能够不借助 cache-loader
,间接在babel-loader
前面加上?cacheDirectory=true
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader?cacheDirectory=true']
}
]
}
}
hard-source-webpack-plugin
hard-source-webpack-plugin
用于开启模块的缓存。
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin")
module.exports = {plugins:[ new HardSourceWebpackPlugin() ]
}
应用 hard-source-webpack-plugin
后,二次构建速度大略晋升了 90%。
include/exclude
通常来说,loader 会解决合乎匹配规定的所有文件。比方 babel-loader,会遍历我的项目中用到的所有 js 文件,对每个文件的代码进行编译转换。而 node_modules 里的 js 文件基本上都是转译好了的,不须要再次解决,所以咱们用 include/exclude 来帮咱们防止这种不必要的转译。
module.exports = {
module:{
rules:[
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/
// 或者 include: [path.resolve(__dirname, 'src')]
}
]
},
}
include 间接指定查找文件夹,比 exclude 效率更高,更能晋升构建速度。
动态链接库
下面的 babel-loader 能够通过 include/exclude,防止解决 node_modules 里的第三方库。
但如果将第三方库代码和业务代码都打包进一个 bundle 文件,那么解决这个 bundle 文件的插件,比方 uglifyjs-webpack-plugin、terser-webpack-plugin 等,就没方法不解决外面第三方库内容。
其实第三方库代码根本都是成熟的,不用作什么解决。因而,咱们能够将我的项目的第三方库代码分离出来。
常见的解决形式有三种:
- Externals
- SplitChunks
- DllPlugin
Externals 能够防止解决第三方库,但每一个第三方库都得在 html 文档中减少一个 script 标签来引入,一个页面过多的 js 文件下载会影响网页性能,而且有时咱们只应用第三方库中的一小部分性能,用 script 标签全量引入不太正当。
SplitChunks 在每一次构建时都会从新构建第三方库,不能无效晋升构建速度。
这里举荐应用 DllPlugin 和 DLLReferencePlugin(配合应用),它们是 webpack 的内置插件。DllPlugin 会将不频繁更新的第三方库独自打包,当这些第三方库版本没有变动时,就不须要从新构建。
应用办法:
- 应用 DllPlugin 打包第三方库
- 应用 DLLReferencePlugin 援用 manifest.json,去关联第 1 步中曾经打好的包
- 首先,新建一个 webpack 配置文件
webpack.dll.js
用于打包第三方库(第 1 步)
const path = require('path')
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry: {three: ['three', 'dat.gui'] // 第三方库数组
},
output: {filename: '[name].dll.js', //[name]就是在 entry
path: path.resolve(__dirname, 'dist/lib'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({name: '[name]',
path: path.resolve(__dirname, 'dist/lib/[name].json') //manifest.json 的寄存地位
})
]
}
打包好后,能够看到,在 dist
目录下减少了一个 lib 文件夹。
- 而后,咱们在
webpack.base.js
做一下批改,去关联第 1 步中曾经打好的包(第 2 步)
module.exports = {
plugins:[
// 批改 CleanWebpackPlugin 配置
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['!lib/**' // 在每次分明 dist 目录时,不清理 lib 文件夹的内容]
}),
// dll 相干配置
new webpack.DllReferencePlugin({
// 将 manifest 字段配置成咱们第 1 步中打包进去的 json 文件
manifest: require('./dist/lib/three.json')
})
]
}
再次打包后能够看到,相比于一开始整个我的项目的体积 9.11MB,体积减小了 90%,因为这是一个多页面打包(多页面打包配置)的利用,每个页面都援用了体积宏大的 three.js 外围文件
,咱们把体积最大的three.js 外围文件
从每个页面的 bundle 中抽离进去后,bundle 的体积大大减小。
再来看看构建工夫:相比于应用 DllPlugin 之前,工夫缩小了 30%。
不仅仅是第三方库,业务代码中的根底库也能够通过进行 DllPlugin 拆散。
优化构建体积
代码宰割
拆散第三方库和业务代码中的根底库,能够防止单个 bundle.js 体积过大,加载工夫过长。并且在多页面构建中,还能缩小反复打包。
常见的操作是通过 SplitChunks(之前的文章曾经具体地写过了:SplitChunks)和 动态链接库(如上所示),这里都不再赘述。
动静 import
动静 import 的作用次要缩小首屏资源的体积,非首屏的资源在用到的时候再去申请,从而进步首屏的加载速度。一个常见的例子就是单页面利用的路由治理(比方vue-router
)
{
path: '/list',
name: 'List',
component: () => import('../views/List.vue')
},
不是间接 import 组件(import List from '../views/List.vue'
),那样会把组件都打包进同一个 bundle。而是动静 import 组件,但凡通过 import()
援用的模块都会打包到独立的 bundle,应用到的时候再去加载。对于性能简单,又不是首屏必须的资源都举荐应用动静 import。
<span @click="loadModal">show 弹窗 </span>
/***
methods: {loadModal(){import('../modal/index.js')
}
}
***/
treeShaking
应用 ES6 的 import/export 语法,并且应用上面的形式导入导出你的代码,而不要应用 export default。
// util.js 导出
export const a = 1
export const b = 2
export function afunc(){}
或
export {a, b, afunc}
// index.js 导入
import {a, b} from './util.js'
console.log(a,b)
那么在 mode:production
生产环境,就会主动开启 tree-shaking
,移除没有应用到的代码,下面例子中的afunc 函数
就不会被打包到 bundle 中。
代码压缩
罕用的 js 代码压缩插件有:uglifyjs-webpack-plugin
和 terser-webpack-plugin
。
在 webpack4 中,生产环境默认开启代码压缩。咱们也能够本人配置去笼罩默认配置,来实现更定制化的需要。
v4.26.0 版本之前,webpack 内置的压缩插件是 uglifyjs-webpack-plugin,从 v4.26.0 版本开始,换成了 terser-webpack-plugin
。咱们这里也以terser-webpack-plugin
为例,和一般插件应用不同,在 optimization.minimizer
中配置压缩插件
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true, // 开启并行压缩,能够放慢构建速度
sourceMap: true, // 如果生产环境应用 source-maps,则必须设置为 true
})
]
}
}
雪碧图
雪碧图将多张小图标拼接成一张大图,在 HTTP1.x 环境下,雪碧图能够缩小 HTTP 申请,减速网页的显示速度。
用于合成雪碧图的图标体积要小,较大的图片不倡议拼接成雪碧图;同时要是网站动态图标,不是通过 ajax 申请动静获取的图标。所以通常是作为网站 logo、icon 之类的图片。
开发时,能够是 UI 提供雪碧图,然而每新增一个图标,就要从新制作一次,从新计算偏移量,比拟麻烦。通过 webpack 插件合成雪碧图,就能够在开发时间接应用单个小图标,在打包时,自动合成雪碧图,并主动主动批改 css 中的 background-position
的值。
上面,咱们借助 postcss-sprites
来自动合成雪碧图。
首先,在 webpack.base.js
中配置postcss-loader
:
//webpack.base.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['vue-style-loader','css-loader', 'postcss-loader'] // 配置 postcss-loader
},
{
test: /\.less$/,
use: ['vue-style-loader','css-loader', 'postcss-loader', 'less-loader'] // 配置 postcss-loader
}
]
}
};
而后在我的项目根目录下新建.postcssrc.js
,配置postcss-sprites
。
module.exports = {
"plugins": [require('postcss-sprites')({
// 默认会合并 css 中用到的所有动态图片
// 应用 filterBy 指定须要合并的图片,比方这里这里只合并 images/icon 文件夹下的图片
filterBy: function (image) {if (image.url.indexOf('/images/icon/') > -1) {return Promise.resolve();
}
return Promise.reject();}
})
]
}
默认会把图片合并到名为 sprite.png
的雪碧图中。
在 css 中间接指定小图标当背景:
.star{
display: inline-block;
height: 100px;
width: 100px;
&.l1{background: url('../icon/star.png') no-repeat;
}
&.l2{background: url('../icon/star2.png') no-repeat;
}
&.l3{background: url('../icon/star3.png') no-repeat;
}
}
打包实现后能够看到,主动批改了 background-image
和background-position
。
gzip
开启 gzip 压缩,能够减小文件体积。在浏览器反对 gzip 的状况下,能够放慢资源加载速度。服务端和客户端都能够实现 gzip 压缩,服务端响应申请时压缩,客户端利用构建时压缩。但压缩文件这个过程自身是须要消耗工夫和 CPU 资源的,如果存在大量的压缩需要,会加大服务器的累赘。
所以能够在构建打包时候就生成 gzip 压缩文件,作为动态资源放在服务器上,接管到申请后间接把压缩文件返回。
应用 webpack 生成 gzip 文件须要借助compression-webpack-plugin
,应用配置如下:
const CompressionWebpackPlugin = require("compression-webpack-plugin")
module.exports = {
plugins: [
new CompressionWebpackPlugin({test: /\.(js|css)$/, // 匹配要压缩的文件
algorithm: "gzip"
})
]
}
打包实现后除了生成打包文件外,还会额定生成 .gz 后缀的压缩文件。能够看出,gzip 压缩文件的体积比未压缩文件的体积小很多。