前言

今天我们来学习如何用webpack来优化项目。

优化方式

优化无用的css

顾名思义,就是把没有用到的css样式优化掉,可以减少构建包的大小

主要是通过插件purgecss-webpack-plugin来实现。

1
// 这里想弄个栗子的,但是这个`vue-cli-plugin-purgecss`的插件一直装不上只能作罢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 正常方式使用
const PATHS = {
src: path.join(__dirname, 'src')
}
const config = {
plugins: [
new PurgecssWebpackPlugin({
// 可以直接这样用
paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true}),
// 也可以用globAll来选择具体使用的目录
paths: globAll.sync([
`${PATHS.src}/index.html`,
`${PATHS.src}/styles/*`,
`${PATHS.src}/*/*.vue`,
]),
})
],
}

提取CSS到单独的文件里

为什么要提取css到单独的文件中呢?项目比较大时,样式会特别多且特别复杂。如果仍然用'style-loader', 'css-loader', 'sass-loader'这种方式的话会大幅延长首页加载时间,且很多样式并不需要在首页加载。所以我们需要对CSS做优化且优化点就在于分离不同模块的样式且最好分离出的模块能够按需加载。

这里就需要用到一个插件来对这种现象进行优化:webpack4的话使用mini-css-extract-plugin,webpack3的话使用extract-text-webpack-plugin
这两个插件的作用是一样的,但是前者功能更加完善(毕竟是后面出的,肯定优化更好一些)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const PATHS = {
src: path.join(__dirname, 'src')
}
const isDev = process.env.NODE_ENV === 'development'
const config = {
plugins:[
// MiniCssExtractPlugin插件支持css按需加载和sourceMap
new MiniCssExtractPlugin({
filename: isDev ? [name].css : [name].[contentHash].css,
chunkFilename: isDev ? [id].css : [id].[contentHash].css
}),
],
modules:{
rules:[
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
]
}
}

自动添加前缀

由于样式可能会存在浏览器兼容问题,所以我们会需要在样式中添加前缀,但是你也不可能所有的样式都写一边。这里我们可以使用postcss来做这件事情,那么在webpack中我们就需要用到postcss-loaderautoprefixer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const config = {
plugins:[
// MiniCssExtractPlugin插件支持按需加载和sourceMap
new MiniCssExtractPlugin({
filename: [name].[contentHash].css,
chunkFilename: [id].[contentHash].css
}),
autoPrefixer({
browser: [
'last 10 Chrome versions',
'last 5 Firefox versions',
'Safari >= 6',
'ie> 8'
]
})
],
modules:{
rules:[
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
]
},

}

压缩文件

首先,webpack4中默认在production时会开启压缩。但是这个压缩是对js文件的压缩,所以css文件我们需要通过optimize-css-assets-webpack-plugin来完成。But,由于配置css压缩时会覆盖掉webpack的默认压缩配置, 所以需要额外引入js压缩插件terser-webpack-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

const config = {
optimization:{
minimize: true,
minimizer:{
new TerserPlugin({
parallel: 4, // 并行压缩
}),
new OptimizeCssAssetsPlugin({})
}
}
}

JS抽取公共代码

很多时候,一些公共的代码/库会在应用内各个模块内都用到。但是按照之前的逻辑,webpack打包的时候会把这些代码多次打入进去。这样就会使得构建包的体积增大。所以,我们需要能够把公共代码提取出来,这样子只需要加载一次,其它地方用的时候都要不需要重复加载。

这里我们主要使用的是splitChunkPlugin来抽取公共代码,不过这个插件不需要我们显式的引入使用,在webpack4中可以通过optimization.splitChunks来配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// webpack官网给的默认配置。
const config = {
optimization:{
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}

chunks

chunks内有['all', 'async', 'initial']三种模式, 默认为async

  • async:只从异步加载的模块里进行拆分
  • initial:只从入口模块进行拆分
  • all:既从异步加载的模块里进行拆分也从入口模块进行拆分

配置多入口

多入口其实本质上也是一种分离代码的方式。也是减少初始化需要加载的内容数量。然后是利用webpack.config.entry来配置。

1
2
3
4
5
6
7
8
9
10
11
const config = {
// 两个入口 main,other
entry:{
main: 'main.js',
other: 'other.js',
},
output:{
// 只需要这样子写就可以了。 输出的也是main,other
filename: '[name].js',
}
}

noParse

对于一些引用到的第三方库,我们可以很清晰知道它不会和其它模块产生依赖时。就可以用到noParse。它的作用就是让webpack不去解析这些库的依赖关系。

1
2
3
4
5
const config = {
module:{
noParse: /jquery|bootstrap/
}
}

IgnorePlugin

对于引用的一些第三方库,例如momentjs,dayjs。他们内部都是自带国际化处理的,那么就会有很多对应的语言包。但是很多时候我们往往不需要这些语言包。这个时候我们就可以通过使用IgnorePlugin来忽略掉所有的语言包,当然,在这之后我们还得按需引入实际要用到的语言包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// entry.js
import moment from 'moment';
// 设置为中文
moment.locale('zh-CN');

// 在这之前我们得通过阅读源码找到moment的语言包目录。这里不细讲了。
// 最后可以找到moment的语言包目录是 /locale

// webpack.config.js
const config = {
plugins:[
// ...其它插件
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
}

// entry.js
import moment from 'moment';
// 需要手动引入该语言包
import 'moment/locale/zh-cn';
// 设置为中文
moment.locale('zh-CN');

DLL预编译优化

对于一些不会经常更新的库,例如vue,react等等。这些库我们一般不会更新版本,但是每次打包时都要解析。这样是会影响到webpack的打包速度的。而且就算我们做了拆分,提升的也只是在页面中访问的速度,而不是webpack打包的速度。所以为了提高webpack构建的速度,我们可以采用类似于dll(动态链接库)的方式来提升构建速度。

这样子做的本质其实就是相当于我提前把这一部分不会经常更新的库先打包一遍,这样子我之后webpack打包就只需要打包另外一部分内容。相当于减少了打包的工作量。

DLL优化中我们主要会用到两个插件,一个是DLLPlugin,一个是DLLReferencePlugin
DLLPlugin呢主要是用来生成dll内容的。dll内容包括打包生成的js文件以及manifest.jsonDLLReferencePlugin会使用该json文件来做映射依赖性,同时webpack通过这个文件可以知道哪些文件已经提取打包好了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// dll.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
mode: 'none',
entry:{
vue: [
'vue/dist/vue',
'vue-router',
'vuex',
'axios'
]
},
output:{
path: path.resolve(__dirname, './public/static/dll'),
filename: '[name]_[hash].dll.js',
library: '[name]_[hash]'
},
plugins:[
new webpack.DllPlugin({
name: '[name]_[hash]',
path: path.resolve(__dirname, './public/static/dll', '[name]-manifest.json'),
context: __dirname
})
]
}

// webpack.config.js
// 引入之前生成的manifest.json
const dllJson = require('./public/static/dll/vue-manifest.json');

const config = {
plugins:[
// ... 其它插件
// 这里只是成功的引入了dll依赖关系,并没有动态的把它导入到项目里。
new webpack.DllReferencePlugin({
manifest: dllJson,
}),
new HtmlWebpackPlugin({
template: 'index.html',

// 导入dll到html中方案1
vendor: '/static/dll/' + dllJson.name + '.dll.js'
}),

// 导入dll到html中方案2
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, `../dist/static/dll/${dllJson.name}_dll.js`)
})
]
}
// 导入dll到html中方案1
// index.html 注入dll.js
`
<body>
<div id='app'></div>
<script src="<%= htmlWebpackPlugin.options.vendor %>"></script>
</body>
`

TreeShaking

TreeShaking的意思就是删除没有用到的冗余代码,可以理解为一个提纯操作。把使用到的代码提取出来然后在打包。可以通过package.json中的sideEffects属性或者是webpack.config.module.rules来设置。

sideEffects有三个值:

  • true: 不做tree-shaking
  • false: 都可以做tree-shaing
  • []:对不符合数组内结果的内容做tree-shaking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// package.json

// 不符合该匹配结果的都会做tree-shaking
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]

// webpack.config.js
const config = {
module:{
rules:[
{
include: path.resolve("node_modules", "lodash"),
// 做tree-shaking
sideEffects: false,
},
{
include: path.resolve("src/view/home/index.vue"),
// 不做tree-shaking
sideEffects: true,
}
]
}
}

其中还有,为了更好的tree-shaking。我们在平时coding的时候,可以这样子做:

  • import {cloneDeep} from 'lodash' 代替 import _ from 'lodash'
  • import cloneDeep from 'lodash/cloneDeep' 代替 import _ from 'lodash'
  • 像项目里常用的的方法库这种,尽量这样子写也有助于tree-shaking
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export const foo(){
    // ...
    }
    export const foo1(){
    // ...
    }
    export default {
    foo,
    foo1,
    }

多进程优化构建

首先,由于js本身是一个单线程的语言,所以webpack打包的时候,任务只能一个一个的完成。但是我们又想要webpack能够同时完成多个任务。那怎么办呢?

我们可以通过HappyPack,thread-loader(官方推荐)等插件来使得webpack能够做到多线操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const isDev = process.env.NODE_ENV === 'development';
const HappyPack = require('happypack');
const os = require('os');
const HappyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
const config = {
module:{
rules:[
{
test: /\.js$/,
exclude: /node_mudoles/,
use: 'happypack/loader?id=happyBabel'
},
{
test: /\.scss$/,
use: 'happypack/loader?id=happyScss'
}
]
},
plugins:[
// ...
new HappyPack({
id: 'happyBabel', // loaders声明的对应的id
threads: 4, // 线程数
loaders:['babel-loader'] // loaders
}),
new HappyPack({
id: 'happyScss',
threads: 2,
loaders:[isDev ? 'style-loader' : MinicssExtractPlugin.loader,'css-loader','sass-loader']
}),
]
}

注:由于happyPack作者自身原因(对于js逐渐缺少兴趣),happypack现在已经停止维护。webpack4及以后官方都推荐使用thread-loader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const isDev = process.env.NODE_ENV === 'development';
const config = {
module:{
rules:[
{
test: /\.js$/,
exclude: /node_mudoles/,
use: [
{
loader: 'thread-loader',
options:{
workers: 2,
}
},
{
loader: 'babel-loader',
}
]
},
{
test: /\.scss$/,
use: [
{
loader: 'thread-loader',
options:{
workers: 2,
}
},
isDev ? 'style-loader' : MinicssExtractPlugin.loader,
'css-loader',
'sass-loader'
]
}
]
},
}

另注:由于进程启动大概需要600ms,且进程的通信也是有开销的。所以需要做一个取舍。如果项目比较小的话,多进程可以不开。

总结

其实总的来说,优化其实可以分为几个大的方面。

第一是对包体积的优化,主要包括优化冗余css,提取公共代码,tree-shaking,压缩文件)等等方式。其主要目的就是为了使bundle包更小。

第二的话就是优化对bundle包的加载(其实也就是优化首页加载),优化加载的核心思想我认为是分离和按需。主要就是分割完整代码成多个代码片(其实就是main.js ==> bundle1.js,bundle2.js这样),分割出来的文件颗粒度更细。然后再按需加载,这样可以保证分割出的文件只在我需要用到的时候才会加载。主要包括提取css到单独文件中(减小了首页js大小)。

还有一些其它的优化方式,感觉不属于以上两种。但是感觉现在自己说不太明白。不能很好的表达,暂时先不多做赘述了。

最后想说的就是,优化也是一门技术活。需要懂得灵活运用。而不是啥也不管直接把所有的优化方式都弄进来。需要根据项目大小和进度来进行调整。

相关文章

【Webpack】基础配置