前言
在之前的webpack学习内容中,介绍了使用webpage打包项目的基础构建,其中讲解了对js、css文件的压缩。这其实可以视为项目体积优化的一部分,下面针对体积优化进行进一步的学习。
注:以下功能都是基于Webpack版本^4.43.0
调试。
量化分析
在基础构建中,我们会将项目代码和第三方代码合并打包,统一输出一个bundle
文件中。我们若需要对该文件进行优化,思路应该是对其内的模块进行删减、合并(重复内容)。
那么,首先我们需要知道bundle
文件各个模板的大小情况,这里需要使用插件webpack-bundle-analyzer
,其会输出交互式可视化树图表示bundle
文件的大小。
注:也可以使用webpack --profile --json > stats.json
命令(不推荐该方案),在生成的stats.json
中查看bundle
文件的大小,也可以在官方分析工具中进行分析
// webpack.prod.config.js
const baseWebpackConfig = require('./webpack.base.config');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const webpackConfig = merge(baseWebpackConfig, {
...,
plugins: [
new BundleAnalyzerPlugin(),
...,
],
})
module.exports = webpackConfig;
这样,当我们执行npm run build
命令后,会打开http://127.0.0.1:8888/
:
这样,我们就能很直观的分析出哪个文件的体积最大,其中又是哪个模块的体积最大。
文件压缩
当我们需要优化体积处理时,最初想到的一般是对文件进行压缩。在项目中除了使用gzip
来进行压缩文件外,Webpage中也可以对js、css、html文件进行压缩。(可以参考之前的文章开发过程中的基础构建)
- js文件,设置
mode
的值为production
,Webpage会自动压缩 - css文件,使用插件CssMinimizerWebpackPlugin
- html文件,设置
mode
的值为production
,Webpage会自动压缩 - 图片,使用插件image-webpack-loader自动压缩图片
代码拆分
CDN
在实际项目中,我们可以发现在最后生成的bundle
文件中,第三方库所占的比重很大。我们可以通过配置externals
,将其中常用的第三库(比如vue、vue-router、vuex等)抽离出来,放置在CDN
中,通过<script>
来引入,减少打包文件体积。
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
// webpack.prod.config.js
module.exports = {
...,
externals: {
// jquery,即key为项目逻辑代码中引入的第三方库名称
// jQuery,即value,表示通过script引入之后的全局变量
jquery: 'jQuery'
}
};
// 项目逻辑代码
import $ from 'jquery';
...
防止重复
SplitChunksPlugin插件可以将公共的依赖模块提取到已有的 entry chunk 中,或者提取到一个新生成的 chunk。
webpack 将会基于以下条件自动分割代码块:
- 新的代码块被共享或者来自 node_modules 文件夹
- 新的代码块大于 30kb (在 min+giz 之前)
- 按需加载代码块的请求数量应该 <=5
即加载 chunk 所发送的请求不能超过 5 个。
举个例子: a.js 依赖了 1.js、2.js、3.js、4.js 这四个文件,且这四个文件满足了代码分割的前两个条件 (被共享且大于 30kb)。这个时候就会分割出五个 chunk,分别是:a.chunk.js、1.chunk.js、2.chunk.js、3.chunk.js、4.chunk.js。此时加载 chunk~a 的并发请求数就恰好是 5 个。
复制代码如果 a.js 又依赖了 5.js,,这个时候并不会分割出 5.chunk,因为如果分割出 5.chunk,那么加载 a.chunk 的请求就是 6 个了, 5.js 的内容会被合并到 a.chunk 中。
-
页面初始化时加载代码块的请求数量应该 <=3
这里的逻辑和上面类型,只不过是针对
entry
的。
并且,官方在optimization
下的splitChunks
下提供了默认配置(具体配置说明可以查看这里):
// webpack.config.js
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 这表明将选择哪些块进行优化。 "initial" | "all"(推荐) | "async" (默认) | 函数
minSize: 30000, // 生成块的最小大小(以字节为单位)
maxSize: 0,
minChunks: 1, // 拆分前必须共享模块的最小块数
maxAsyncRequests: 5, // 按需加载时并行请求的最大数量
maxInitialRequests: 3, // 入口页面的最大并行请求数
automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称生成名称(例如vendors~main.js)。此选项使您可以指定用于生成名称的定界符。
name: true, // 拆分块的名称。boolean: true | function (module, chunks, cacheGroupKey) | string
cacheGroups: { // 这里开始设置缓存的 chunks 缓存组,splitChunks就是根据cacheGroups去拆分模块的
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
动态导入
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。
这里我们使用第一种方案动态引入。其引入方法为:
import(/** webpackChunkName: "lodash" **/ 'lodash').then(_ => {
// doSomething
})
接下来,我们还需要修改下webpack配置,添加chunkFilename
:
// webpack.config.js
module.exports = {
output: {
filename: '[name].bundle.js',
+ chunkFilename: '[name].bundle.js', // [name]就是你在import时webpackChunkName的值
path: path.resolve(__dirname, 'dist')
},
};
其他优化
tree-shaking
如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。
// utils.js
const add = (a, b) => {
return a + b;
}
const minus = (a, b) => {
return a - b;
}
export {
add,
minus
}
//index.js
import {add, minus} from './utils';
add(1, 2);
在项目中,minus
不会被构建到bundle
中。
scope hosting 作用域提升
JavaScrct中有函数提升和变量提升,会把函数和变量声明提升到当前作用域的顶部。webpack中scope hosting
也是类似,将模块进行提升。
scope hosting
会分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,这样减少了引入模块的开销。但前提是不能造成代码冗余,因此只有那些被引用了一次的模块才能被合并。
// webpack.config.js
module.exports = {
plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
};
由于 Scope Hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。
动态 Polyfill 服务
babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,比如:
- 全局对象:Promise、WeakMap 等。
- 全局静态函数:Array.from、Object.assign 等。
- 实例方法:比如 Array.prototype.includes 等。
此时,需要引入babel-polyfill来模拟实现这些对象、方法,一般也称为垫片。
babel-polyfill
由于是一次性全部导入整个polyfill
,所以导致文件很大,所以我们使用动态 Polyfill 服务进行优化。
每次打开页面,浏览器都会向Polyfill Service发送请求,Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载Polyfill的效果。