可视化主要是使用两个插件做耗时和体积上的前后对比,新手上岸,有问题希望能够指出,感激不尽...
目前基于webpack 3.12.0 ,当然webpack4相对来说改动较大做了不少优化,在本次优化后会连载直接升级至4到底提升了多少,做个 diff
目录:为什么要打包
webpack构建流程图
如何进行 code spliting
优化实战与效果比对
其他考虑到但未选择的优化方案
为什么要打包
打包之前或许应该明确一下为什么要打包,在 模块化学习 里有说到,es6 在语言层面上解决了js模块化的问题,但是浏览器无法识别 import export 等新的语法,必须经过babel转化,而babel 转化之后最终输出的是 commonJs,而 commonJs 只能在 node 环境里才能运行,所以我们必须做点事情可以使得浏览器能够运行我们的项目文件,所以此时 parcel webpack rollup snowpack 等打包器越来越多,生态越来越庞大,webpack里各种妖魔鬼怪。。。在编译转化,压缩,合并,混淆,速度,耗时优化等方面越走越远。
webpack 构建流程是怎样的
webpack 构建分为三部分:初始化 - 编译 - 输出初始化:读取与合并配置参数,加载 Plugin,实例化 Compiler,注册和调用插件
编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到本地。
webpack打包是一个串行的过程,以 vue 为例演示打包流程图:
loader 作用:转换不能识别的语法,主在编译,比如babel-loader转化 js 高级语法;vue-loader 将 .vue 文件中的语言块应用在相应的 rules;less-loader 处理less语法
plugin 作用:压缩、合并、拆分、混淆,主在优化,UglifyJsPlugin 压缩混淆js,OptimizeCSSPlugin 优化压缩 css
构建的过程中 plugin会对自己感兴趣的步骤上执行特定的逻辑,从而改变输出内容
(有人建议使用 tapable 解释构建流程会更好一点,但确实不太有时间,总之tapable 是webpack的核心,本以为compiler就是底层了,没想到他是 tapable的实例,这篇文章可以看一下 webpack4.0源码分析之Tapable)
如何进行 code spliting
一般的拆包方式:分离 第三方模块
抽取公共代码
动态加载
说明一下当前项目情况:vue项目,两个入口,里面有不少的第三方模块,比如 lodash,jquery,aixos,图表类,裁剪图片的,编辑器的,素材库等,使用 vue-router 划分出20个异步加载模块;
1、分离第三方模块:就是将引入的三方库单独打包,因为这些可识别且不会经常变化,一般名为 vendor
2、抽取公共代码:我们有两个入口,直接打包会生成两个文件,包含着大量重复的代码包的体积也比较大,可以从 entry1 和 entry2 作为 source chunk 提取一个common chunk;也可以是从多个异步模块中提取被引用了 n 次的模块
3、动态加载:也就是在需要的时候加载一些代码,凡是使用 import() 或 require.ensure() 加载的模块都会被打成异步包,webpack内部通过 jsonp 的方式引入这些模块
通过以上方式基本可以拆分出来一些,但有时会发现某些包还是比其他看起来大很多,这一般是应用程序的核心页面,此时可以采用 CommonsChunkPlugin的 minChunks 设置规则将其单独提取出来
优化实战
使用 speed-measure-webpack-plugin 测试 plugin 与 loader 花费时间
const SpeedMeasureWebpack = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasureWebpack(); // 监控面板,现在是各个插件loader花费时间
module.exports = smp.wrap(webpackConfig);
使用 webpack-bundle-analyzer 测试打包情况
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin({ analyzerPort: 8899 }));npm run build --report 后会打开 localhost:8899 查看当前各个chunk的size与占比,具体的就不贴图了,非个人项目
1、优化前应该先整体审视一下,深入项目代码或许会发现有很多三方库引入之后并没有使用,或者很多的图片和字体并没有使用,这时候可以选择性删除掉这些无用的依赖,可能会发现已经有明显提升,这一步非常重要
2、从 webpack-bundle-analyzer 里你会发现第三方库导致打出来的包非常大,这时候去网上搜各种方法,先试一下炒的最热的 dllPlugin(webpack内置插件),据说可以明显提升编译速度,dll 对某些三方库做了动态链接,在打包的时候无需编译可以提升编译速度
// 新建 webpack.dll.conf.jsconst path = require("path");
const webpack = require("webpack");
module.exports = {
entry: {
"vendor_app": [ // 入口1需要的三方 "***",
"***"
],
"vendor_common": [ // 入口2需要的三方 "vue",
"****",
"vue-router",
"vuex",
"lodash",
"axios",
"jquery"
]
},
output: {
path: path.resolve(__dirname, "..", "static/dll"), // 打包后文件输出的位置 filename: "[name].dll.js",
library: "[name]_library" // 和 DllPlugin的name保持一致 },
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, "..", "static/dll/[name]-manifest.json"),
name: "[name]_library",
context: __dirname // 必填 })
]
};
// webpack.base.conf.jsconst manifestApp = require("../static/dll/vendor_app-manifest.json");
const manifestCommon = require("../static/dll/vendor_common-manifest.json");
const webpack = require('webpack');
plugins: [
new webpack.DllReferencePlugin({ // 动态链接 manifest: manifestApp
}),
new webpack.DllReferencePlugin({
manifest: manifestCommon
}),
]
// 将打包出来的文件动态插入htmlnew HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
chunks: ['manifest', 'app', 'common'],
dllPath: path.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory,'/dll/')
}),
// 修改index.html
// package.json 添加"dll": "webpack --config build/webpack.dll.conf.js",
npm run dll, 在 static/dll 下多了四个文件: 两个js, 两个manifest文件
npm run build 看下效果
速度提升约3s,打出来的包没有变化,但在最终的体积上加了 3.34 + 1.58 MB,所以有些得不偿失
这里本想喷一下dllplugin 的,结果这次还真的快了些,之前测了几次都是变慢的,可能和其他优化手段一起产生作用的效果,还好我有备案,群里也问了下,有几个人都说测了变慢了
因此我打算尝试其他的方法...
使用 Entry + commonPlugin 将三方库单独拿出来:
var packagejson = require('./package.json');
entry: {
vendor: [
'vue',
'vuex',
'vue-router',
'jquery',
'axios',
'lodash',
'element-ui'
],
// 或者 Object.keys(packagejson.dependencies) 将 生产环境要用的一股脑都放在vendors app1: ..
app2: ..
}
....
// 将 vendor 引入模板new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'manual',
chunks: ['manifest', 'common', 'vendor', 'app']
}),
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor'],
filename: '[name].js'
}),
时间上多了1s,可以看到是 uglifyjsplugin 的时间长了
压缩太慢了,试一下 resolve.modules 看会不会有变化
resolve: {
..
modules: [resolve('node_modules')]
// 指定第三方库的位置, 这里遇到第三方就从 node_modules 里找,而不是一层一层向上查找}
效果要不要这么明显..
再试一下 module.noParse 会不会有提升
module: {
// 避免解析未使用模块化的库
noParse: /jquery|lodash|axios/
}
报错了。。
一个一个删掉看下谁不对,先删掉 axios 试下
呀,成了,提升了3s!
使用一下 excludes 直接排除掉不需要loader的模块
rules: [
{
exclude: resolve('node_modules') // 所有的规则下我都加了 这句
}
]
结果 有个字体文件和js文件报错,那就把你们删掉吧,效果如下
到此,感觉快了不少, npm run dev试一下,报错啦
找了一通发现是 noParse 了 lodash ,但是 xx 模块 require("lodash/cloneDeep"). 避免lodash的解析报错,那也删掉好了
慢了3s。。。 那我把jquery 也干掉看看
看来还是 jquery还是产生了一丁点的影响的,
回过头来发现项目里到处都是 import $ from 'jquery', import _ form 'lodash' 的字眼,lodash 被47个文件引入,jquery 被82个文件引入, axios 被 12个文件引入
此时可以试下 providePlugin,并且删掉几个 import 测试下
new webpack.ProvidePlugin({
$: "jquery",
_: "lodash",
axios: "axios"
})
// .eslintrc.jsglobals: {
'$': true,
'_': true,
'axios': true
},
这里的 1min 5.92secs 是因为在测试providePlugin 之前测了点其他东西,不过最后又改回上一步的配置了,感觉回到解放前,前面那么顺利会有编译缓存的问题,这里主要对比 ProvidePlugin的作用,发现多了 2s,不过我是可以妥协的
npm run dev 发现也没啥问题,此时部署到测试环境看看还能不能运行,因为之前出过问题,我可不想到优化完线上一看一堆报错还不知道是哪的问题。。。。
推代码,隐隐感觉有些不妙...
...
果然,一下回到解放前
我不相信这是真的,我再推一遍
...
...
更多了...
我得试下其他分支,是不是docker的问题
...
对比一下,没白干就行
至此,这些优化手段反反复复,好在最终速度有所提升
现在试下 happyPack吧,虽然貌似没有再维护了
npm i happypack -D
const HappyPack = require("happypack");
const HappyPackThreadPool = HappyPack.ThreadPool({ size: 5 });
module: {
noParse: /jquery/,
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
// 将对.vue 文件的处理转交给 id 为 vue 的HappyPack实例 // loader: 'vue-loader', use: ['happypack/loader?id=vue'],
exclude: resolve('node_modules')
},
{
test: /\.js$/,
// loader: 'babel-loader', use:['happypack/loader?id=babel'],
include: [resolve('src')],
}
],
plugins: [
new HappyPack({
// 用唯一的标识符id,来代表当前的HappyPack是用来处理一类特定的文件 id:'vue',
loaders:['vue-loader'],
threadPool: HappyPackThreadPool
}),
new HappyPack({
id:'babel',
// babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启 loaders:['babel-loader?cacheDirectory'],
threadPool: HappyPackThreadPool
})
...
]
显著提升。。
对比一下 开发环境,
我不该看的... 我什么也没看见...
...
算了,再给一次机会
看来缓存生效了
到这里,看了下打包的各chunk的占比,发现 vendor 里竟然有业务代码,我明明只添加了三方库,发现是提取 vendor写的有问题
new webpack.optimize.CommonsChunkPlugin({
// name: ['vendor'], name: 'vendor',
filename: '[name].js'
}),现在vendor里只有 三方了
再看占比图,此时发现一个异步包非常醒目,整个 4.44M,里面的node_modules 3.34M,
那可以把它分离出来看看
new webpack.optimize.CommonsChunkPlugin({
async: "editor-async",
chunks: ["editor"],
minChunks: function(module) {
return module.context && module.context.includes("node_modules");
}
}),此时editor.js 被分为 editor-async-app 和 editor.js
拆出来的 editor-async-app 包依然很大,那就多加个异步包吧,抽离下editor-async-app 的 material-library
// import { Material } from 'material-library';import(/* webpackChunkName: "material" */ "material-library").then(res => {
Vue.component("material", res.Material);
});
写完发现不对,import() 的异步组件必须放在入口文件,但是我这个组件只是在某个路由下才会使用,这样写无故多了个请求,让首屏变慢,并且 editor-async-app gzip 后 354k 勉勉强强能接受吧,放弃。。。 等会,又逮到一个更大的 ,text-editor gzip 后 119.98KB
查了一下,在页面 import 了三次,虽然目前只有一个异步chunk在用,但是这个东西还是比较通用性的库,不像上一个很私有,只有在特定的页面才有用,那分出来看看好了
// import Editor from 'text-editor'import(/* webpackChunkName: "textEditor" */ 'text-editor').then(editor => {
window.Editor = editor;
});
这样页面就不用多处import了 , 此时 npm run build --report 一下
并且发现时间上少了,我已经不再相信了,耗时优化是个玄学,我只关注最终效果
再看下整体体积
其实 13.38MB 早有了,当时比较耗时优化来着,所以没贴出来 ,现在拉出来主要为了 说明拆包并不能很明显的减少体积,因为之前已经打过公共包,这样做只是为了减少某个chunk的体积,有资料说 打出来的包最好控制在每个 500K左右,虽然会多一次http请求,但充分利用浏览器的并发请求不好吗
再看看其他的包吧..
发现还有两个包莫名的像:
左边 1.07M, 从 app里的异步模块抽出来引用了至少3次的包
new webpack.optimize.CommonsChunkPlugin({
name: 'app', // 第一个入口,entry1 async: 'vendor-async',
children: true,
minChunks: 3
}),
右边1.05M,是另一个入口 entry2打出来的包
有必要说一下,项目之前从 entry1 和 entry2 抽离了一个common
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
chunks: ['entry1', 'entry2']
}),
此时再捋一遍项目:两个入口 entry1,entry2
entry1 分两大块:一个 管理页 A,并且分为多个路由,几个具体业务页面 B C D E F G ...;这里从 entry1 里抽离公共模块 vendor-async, 如左图
entry2 就是右图,它和 B C D E 共用很多东西,因此上面左右两图 的models 里的很多东西被重复打包
所以如果可以把 异步包 vendor-async 里的models抽离出来,在 B C D E entry2 里异步加载就好了,但目前找不到一种方法可以直接打出来。。还希望评论区见...
但可以预想到 models 拆分出来,entry2会多一个请求,还有一点,models2 里不是所有东西entry2都要,并且entry2 也是很重要的页面,且因为一些静态资源本来就很慢,所以随着models里无用的东西(也就是entry2不需要的)变大,这又是一个新的问题。
综上,1、无法从webpack层面分离models; 2、即使分离也会带来新的问题
所以暂时放弃...
到这里,我觉得应该跳出webpack的层面反观项目代码,毕竟优化本身无法脱离业务
....
这不又让我逮到一个无用的 css,里面包含了大量的字体文件,并且一些静态图片略大,还是一些不怎么重要的图片,打算找个压缩工具压缩一下,走起...
所以 UI 给图片的时候一定要注意,不是给啥我就必须用啥...
下面再看下缓存问题:
之前我将三方库全部打出来一个vendor,3.22MB,gzip 331KB
上缓存:
// webpack.dev.conf.jsnew webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: utils.assetsPath('js/[name]_[hash].js')
})
// webpack.prod.conf.jsnew webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: utils.assetsPath('js/[name]_[chunkhash].js')
}),
可能你想问为什么 dev 要用 hash,prod要用chunkhash
开发环境中使用[chunkhash],会增加编译时间,并且webpack-dev-server 禁止这样操作
那为什么不写在 webpack.base.conf.js ,使用 process.env.NODE_ENV !== 'production' 来判断用哪个,不用写两遍?
我试过了,因为 webpack-merge的缘故 使 commonChunkPlugin 顺序不对 导致在生产环境vendor 里打包了公共模块,这显然不是想要的。正确的应该是, 先从两个入口提取 common,然后从 vendor出口提取vendor
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
chunks: ['entry1', 'entry2']
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: utils.assetsPath('js/[name]_[chunkhash].js')
}),
生产环境下却是需要服务器支持...
让我找人看下,后面补充截图 todo
到此为止,好像优化了不少了,来看下整体优化前后的效果
npm run dev : 提升约10s,提升23.8%
production 下对比:
git clone 放在另一个目录下,因为没有缓存 1min 43,记得刚开始优化前是 1min16来着
这次又 大于 1min,因为 我在build 前清理了一些东西导致缓存失效,重新打包了
loader和plugin上提升了 30s,提升32%
两个再来一次,缓存的原因 同时少了10s
现在我不得不对之前时间上的对比差异产生质疑,因为根本无法确定因为我添加的配置生效还是因为缓存让它变快了...
那现在部署到测试环境看看
对比一下总耗时:提升28%需要说明的是部署时间包含 npm install 的时间,因此 /dev?Dependencies/i 越多越慢
再看下总chunk,可以确认它是不会骗我的:只看gzip还是挺明显的,毕竟删了很多东西
体积压缩 33.3%
其它看到的优化方案
externals:在自己开发一个库的时候会用到,假如我们引用了 vue,但我们不希望webpack 打包 vue,可以使用externals 排除 bundle里的vue,此时用户使用我们的库时就需要手动引入 vue,比如使用elementui 的用户必须引入 vue,它在打包的时候排除了vue;
// config.jsexports.vue = {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
};
// webpack.conf.jsexternals: {
vue: config.vue
}
项目上会联合 cdn 一起使用,index.html 引入几个cdn,在 externals 里排除在打包范围外
因为项目不会只引入一两个库,可能会有五六个cdn,听人说可以使用 cdn + combo的形式,合并http请求,vue+vue-router+vuex一个,element等UI系列一个,说的很好听,你倒是搞啊....
总结与反思一边分秒必争,一边观察包的体积,一遍考虑着实际应用时的场景,这无疑是一场体积,耗时与http请求数之间的博弈
良好的编码习惯很重要... 及时删除引入不必要的包,清理掉不使用字体图片,冗余的代码,适当重构,就是简简单单的优化
前期设计时注重文件组织方式,组件间分工明确,注重功能解耦
网上的花式教程,适合自己的才是最有效的
这两篇文章对比理解分包和优化还是比较好的:
这里无论是在体积和耗时上只提升了百分之二三十,听说 webpack4 + babel7 可以提升98% 的速度,总之就是会提升吧,那可真是要试试了
趁热打铁,预留周末 webpack 3 升 4,babel6升7 可视化....