使用webpack内置的stats
可以统计出构建时间、构建资源清单及资源大小等信息
使用方法:
webpack --env production --json > stats.json
webpack(config, (err, stats) => {
console.log(stats);
});
使用speek-measure-webpack-plugin
插件功能
插件使用:包裹webpack的配置
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp({
// webpack配置
});
使用webpack-bundle-analyzer
以可视化形式展示打包依赖模块的体积。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
构建完成后会启动本地服务,serve 8888端口,浏览器中访问就能看到分析结果。
高版本的webpack和nodejs构建速度更快
webpack4优化原因:
资源并行解析可选方案
{
test: /.js$/,
user: [
{
loader: 'thread-loader',
options: {
workers: 3
}
},
'babel-loader'
]
}
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
new ParallelUglifyPlugin({
// ...
})
]
};
目前webpack官方推荐使用terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4
})
]
}
};
将react、react-dom基础包通过cdn引入,不打入bundle中
使用方法:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: '//path/to/your/cdn-domain/react.min.js',
global: 'React'
},
...
]
});
]
效果
<script type="text/javascript" src="//path/to/your/cdn-domain/react.min.js"></script>
通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。
使用dll时,构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做webpack.config.js
和webpack.dll.config.js
。
步骤:
示例:
使用DLLPlugin进行分包
// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
context: process.cwd(),
entry: {
library: [
'react',
'react-dom',
'redux'
]
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, './build/library'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: './build/library/[name].json'
})
]
};
使用DllReferencePlugin对manifest.json引用
// webpack.config.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./build/library/manifest.json')
})
]
};
引用效果
<script src="/build/library/library.dll.js"></script>
原理
使用DLLPlugin对第三方库打包时候,会生成打包结果和manifest.json文件到指定目录,文件中包含各第三方包的引用关系等信息。
使用DllReferencePlugin插件打包业务代码时候,我们通过配置告诉插件DLLPlugin打包的产物的目录,DllReferencePlugin会分析manifest文件,DLL的包不会参与打包构建过程,并且还生成相关的引用。
使用externals选项可以排除指定的第三方模块,在构建过程中忽略它们。
使用方法示例:
首先在html中引入第三方模块
<script src="https://example-cdn.com/react.min.js"></script>
然后在externals选项里面配置要排除的模块和引用方式
// webpack.config.js
module.exports = {
externals: {
'react': 'React'
}
};
在项目中引用
import React from 'react';
缓存思路
目的:尽可能少构建模块
比如babel-loader不解析node_modules
module.exports = {
rules: {
test: /.js$/,
loader: 'happypack-loader',
exclude: 'node_moudles'
}
};
减少文件搜索范围
通常来讲,同一种类型的文件只能由一个loader处理,那么正常来讲的逻辑应该是,比如我是一个css文件,那么我匹配到test为css后缀的loader我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。
而Oneof语法就是解决这个问题的,使文件一旦匹配上loader之后就立即解析,省去了全盘遍历这个不必要的过程。
如果对于需要多个loader共同解决的文件类型,比如js。那就需要把其中的loader放历Oneof之外,这样才能实现loader也能同时执行到。
Tree-shaking的本质是消除没有用到的代码。主要的效果是,引用了但没有使用的模块,不会被打包到最终的bundle中。
Tree-shaking要求模块是ESM,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。
所谓静态分析就是不运行代码,从语法上对代码进行分析,ES6之前的模块化规范,比如我们可以动态require一个模块,只有执行后才知道是否要引用某个模块,引用的是什么模块(因为require是运行时调用,所以require理论上可以运用在代码的任何地方,而且require支持传入表达式作为参数,只能在运行时才知道引入的是哪个模块),这个就不能通过静态分析去做优化。
webpack默认支持Tree-shaking,如果mode为"production"webpack在构建会做Tree-shaking的操作。
摇树css的基本思路是,给定content和css,分析content中用到的选择器,然后分析css文件中没有用到的选择器,将其移除。
摇树css工具有:
purgecss-webpack-plugin
scope hoisting 是 webpack3 的新功能,直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件“提升到”它的引入者顶部。
在之前版本中,webpack打包会把每个模块用闭包封装,通过__webpack_require__ 引用。
这样会存在问题:
为了解决这两个问题,webpack启用scope hoisting,将每个模块都提升到引入者顶部,这样模块不会因为依赖链路较深而导致调用栈变深,它们都在同一层。
这样就解决了上面的两个问题:
图片压缩实际是使用了基于node库的imagemin或者tinypng API
在webpack中配置image-webpack-loader
,这个loader实际是使用了imagemin进行图片压缩
polyfill的方案:
将babel-polyfill作为一个单独的入口打包
这样做的一个问题是会将所有polyfill代码都打包进去(200k左右),导致代码体积过大
const path = require('path');
module.exports = {
entry: [
'babel-polyfill',
path.resolve(__dirname, './src/index.js')
]
};
选项值为“false”时候,不加入polyfill。
选项值为“entry”时候,将所有polyfill打包进项目。
选项值为“usage”时候,按需加载,并且做了优化:将polyfill的工具方法提取成公共资源,而不会每个 polyfill代码都内联相同的工具方法 。
此外babel-presets中还支持根据支持的浏览器来选择polyfill,这通过"target"属性配置。
// .babelrc
{
"presets": [
"@babel/preset-env",
{
"useBuildIns": "useage"
}
]
}
@babel/runtime
实现polyfill的功能,它分析代码,然后添加相关的polyfill,即实现了polyfill的按需加载。它和上述两种方法的区别是,它在添加polyfill代码时候,不会污染全局变量,而是定义局部方法来实现polyfill。因为这个特点,它更适合用在第三方库中,而上面两种适合用在业务代码项目中。
其缺点在于不支持实例方法的polyfill,如arr.includes(1);
。
由于 @babel/runtime
也是使用内联代码实现polyfill,因此可能多个文件中会内联相同的工具方法。@babel/plugin-transform-runtime
用来解决这个问题,它提取公共的工具方法,每个文件使用时候引入相关的工具方法,这样减少的代码体积。
根据浏览器userAgent选择相应的polyfill,有些浏览器支持的,就不再下发冗余polyfill。
可以使用官方的服务。
或者自建polyfill服务。
可能存在的问题:浏览器ua不准,有些国内浏览器修改ua导致polyfill判断错误,降级方案是下载所有polyfill
https://cdn.polyfill.io/v2/polyfill.min.js
prerender-spa-plugin插件启用无头浏览器,加载项目的路由,并渲染出首屏页面(也可以配置其他路由),然后生成静态页面,保存在指定的目录。
我们的静态资源服务器就可以serve预渲染的页面了。
使用这个插件相当于在构建阶段就渲染好了首屏页面,极大地提升了首屏性能。