最近看到了美团的前端团队的一篇文章,文中提到前端发布仅需10秒,默默的看了一下我们自己的发布时间。。。
先定一个小目标,争取把 Webpack 的打包时间优化到10秒以内吧。
先看一下现在打包一次需要的时间,73013ms,下面开始一步一步见证奇迹:
0. 模块分析
有很多工具提供了可视化的分析,如Webpack-bundle-analyzer、webpack-chart、 webpack-analyse。
以Webpack-bundle-analyzer为例,它提供了一个下图所示的图表,展示了引入的所有模块的大小、路径等信息,可以针对性的做出优化。

使用上也很简单:
1 | // 安装: |
1 | // webpack.config.js 配置 |
运行webpack
命令,会自动在浏览器中打开http://127.0.0.1:8888/
页面,展示可视化图表。
1. 升级到 Weback4.x
Webpack4 带来了极大的性能提升,按照开发者博客中的说法,构建速度最多甚至有高达98%的提升。
升级过程中遇到了一些网上的“Webpack4升级指南”等文章中没有列出的问题,在此分享一下:
1.1 升级 Vue-loader
Vue-loader 目前最新版本为 v15.2.6,使用方式有了很大不同。
现在,我们需要引入一个新的插件 VueLoaderPlugin
,具体使用方式如下:
1 | // webpack.config.js |
同时,在 v15版本的 Vue-loader 中,不再需要单独为 .vue 组件中的模板、CSS等内容单独配置 loader,可以共用普通文件的配置,如下所示:
1 | // webpack.config.js |
1.2 升级 Vue-router
在Vue-router v13.0.0版本中对模块导入做了更新,需要加入 default 配置,如下所示:1
2
3const Foo = () => import('./Foo.vue')
// 需要改为
const Foo = () => import('./Foo.vue').then(m => m.default)
同理:1
2
3const Foo = require('./Foo.vue')
// 需要改为
const Foo = require('./Foo.vue').default
详情可以参考https://github.com/vuejs/vue-loader/releases/tag/v13.0.0
1.3 Chunk 的命名
如果使用 webpackchunkname 魔法注释来命名,需要注意 .babelrc 中 comment 必须为true
1.4 提取 CSS 文件
在 Webpack4 环境下 extract-text-webpack-plugin
需要安装 @next
版本,我们这里直接使用了 mini-css-extract-plugin
来代替。
同时,在 mode 为 development
时,NamedChunksPlugin
和 NamedModulesPlugin
会默认开启,不需要再显式指定。
1.5 本地 mock 处理
2.x版本的 Vue-CLI 启动了一个 express 服务来处理本地数据的 mock,我们尝试做了一些简化,在 webpack-dev-server 的 before
方法中,使用 webpack-api-mocker
插件拦截了请求,读取本地的 mock 数据(JSON文件)返回。其中,mock/index.js
是通过服务启动过程中遍历本地数据文件生成的。
1 | // webpack.dev.conf.js |
1 | // mock/index.js |
升级完成之后,打包时间直接减少了半分钟,达到了44.534秒,离小目标还有很大距离,我们继续。
2. 路由处理(异步加载)
在前面的打包完成的图片中,我们可以看到生成了大量的文件,统计了一下,体积总计高达22.07M,文件近60个。
权衡了诸如加载时间等方面之后,我们决定采用按照一级路由来打包的方式。
具体实现如下: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
29Vue.use(Router);
import Vue from 'vue';
import Router from 'vue-router';
export default new Router({
routes: [{
path: '/a',
component: () =>
import ( /* webpackChunkName: 'a' */ '@/pages/a'),
name: 'a'
}, {
path: '/b',
component: () => import ( /* webpackChunkName: 'b' */ '@/pages/b'),
name: 'b',
children: [{
path: 'b/m',
component: require('@/pages/b/m').default,
name: 'm',
children: [{
path: 'b/m/p',
component: require('@/pages/b/m/p').default,
name: 'p'
}, {
path: 'b/m/q',
component: require('@/pages/b/m/q').default,
name: 'q'
},
]
}]
}]
经过上面的优化,我们将生成的js文件数量减少到了8个,大小减小到了5M.
来看一下打包时间:21.959秒!距离小目标越来越近了。
3. HappyPack/thread-loader
HappyPack 可以将原有的 webpack 对 loader 的执行过程,从单一进程的形式扩展为多进程的模式,从而加速代码构建。使用方式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module: {
loaders: [{
test: /\.less$/,
loader: ExtractTextPlugin.extract(
'style', path.resolve(__dirname, './node_modules', 'happypack/loader') + '?id=less'
)
}]
},
plugins: [
new HappyPack({
id: 'less',
loaders: ['css!less'],
threadPool: happyThreadPool,
cache: true,
verbose: true
})
]
经过测试,在我们的项目中,对 js 和 ts 文件使用 happypack 收益最大。
此处需要注意的是,vue-loader 不支持 happypack,可以使用 thread-loader
来进行加速,同样是新建一个进程来执行 loader 操作,使用方式也很简单:1
2
3
4
5
6
7
8
9 module: {
rules: [{
test: /\.vue$/,
use: [
'thread-loader',
'vue-loader'
]
}]
}
但是在我们的项目中,经过测试,thread-loader 对于打包速度几乎没有影响,是因为它本身的额外开销导致,建议只在极高性能消耗的场景下使用。
完成之后,测试一下打包时间:15.101秒。
4. 缓存loader的执行结果(cacheDirectory/cache-loader)
我们可以对loader做如下配置来开启缓存:1
loader: 'babel-loader?cacheDirectory=true'
或者我们也可以使用 cache-loader :1
2
3
4
5
6
7rules: [{
test: /\.vue$/,
use: [
'cache-loader',
'vue-loader'
]
}]
加入缓存之后,再次测试打包时间:13.915秒。
5. 模块进一步细分(splitChunks)
在 Webpack4 中移除了我们此前常用的 CommonsChunkPlugin
插件,取而代之的是 splitChunks
。splitChunks
的默认配置已经足够我们日常使用,没有特殊需求可以不必特意处理。
我们此处的配置如下(生产环境):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
// styles: {
// name: 'index',
// test: /.stylus|css$/,
// chunks: 'all',
// enforce: true
// }
}
}
}
其中,commons 部分的作用是分离出 node_modules 中引入的模块,styles 部分则是合并 CSS 文件。
经过测试,在我们的项目中,styles 部分使构建时间增加了大约2秒,因此我们放弃了这部分操作。
6. 使用DllPlugin拆分模块
开发过程中,我们经常需要引入大量第三方库,这些库并不需要随时修改或调试,我们可以使用DllPlugin和DllReferencePlugin单独构建它们。
具体使用如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
vendor: [
'axios',
'vue-i18n',
'vue-router',
'vuex'
]
},
output: {
path: path.resolve(__dirname, '../static/'),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'build', '[name]-manifest.json'),
name: '[name]_library'
})
]
}
执行webpack命令,build目录下即可生成 dll.js 文件和对应的 manifest 文件,使用 DLLReferencePlugin 引入:1
2
3
4
5
6plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./build/vendor-manifest.json')
})
]
由于我们的项目中原本已经通过这种方式打包了大部分第三方库,所以这里对打包速度的提升不大,仅仅提升2秒,来到了11.509秒。
7. 精简不必要的模块
在我们的项目中,引入了一些如 moment、lodash 等重型库,然而他们提供的绝大部分功能都是我们不需要的,权衡之后,我们移除了他们,自己实现了部分功能或使用了更小体积的库代替。
移除了这些库之后,我们的打包时间来到了8.921秒!小目标达成了~
但是这距离10秒发布还不够,我们需要争取压缩出更多时间留给发布系统。还能不能继续提升呢?答案是肯定的。
8. 优化模块查找路径
Node.js的模块的载入及缓存机制如下:
载入内置模块
载入文件模块
载入文件目录模块
载入node_modules里的模块
自动缓存已载入模块
如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。
我们可以对搜索过程进行一些优化,比如可以像下面这样指定路径:1
2
3
4
5
6
7
8exclude: /node_modules/, // 排除不处理的目录
include: path.resolve(__dirname, 'src') // 精确指定要处理的目录
resolve: {
modules: [path.resolve(__dirname, 'node_modules')], // 指定node_modules的位置
alias: {
'api': resolve('src/api') // 创建别名
}
}
我们再来看一下时间:7.66秒!
到这里,我们的这次优化基本完成了,其实还有很多可以优化的空间,比如升级一颗 i9 处理器~
这里也只是列举出了一些常见的收益较大的优化方式,希望能对大家有一点帮助,也欢迎有兴趣的同学一起交流。