前言
开发中,webpack文件一般分为3个:
webpack.base.conf.js(基础文件)webpack.dev.conf.js(开发环境使用的webpack,需要与webpack.base.conf.js结合使用)webpack.prod.conf.js(上线环境使用的webpack,需要与webpack.base.conf.js结合使用)
一.优化构建速度
webpack在启动后,会根据Entry配置的入口,递归解析所依赖的文件。这个过程分为搜索文件和把匹配的文件进行分析、转化的两个过程,因此可以从这两个角度来进行优化配置。
1.1 缩小文件的搜索范围
搜索过程优化方式包括:
1. resolve字段告诉webpack怎么去搜索文件,所以首先要重视resolve字段的配置:
参考文档:webpack.docschina.org/configurati…
resolve用来配置模块如何解析。例如,当在 ES2015 中调用 import 'lodash',resolve 选项能够对webpack 查找'lodash' 的方式去做修改(查看模块)。
// webpack.config.js
module.exports = {
//...
resolve: {
// configuration options
}
};
module.export = {
resolve: {
modules:[path.resolve(__dirname, 'node_modules')]
extensions: ['.js', '.jsx'],
mainFiles: ['index', 'child'],
alias: {
'@/src': path.resolve(__dirname, `../src`), // 当看到@/src这个路径或字符串的时候,实际上指向的是../src目录
}
}
}
(1). resolve.modules参考文档:www.webpackjs.com/configurati…
resolve.modules告诉webpack解析时应该搜索的目录。
绝对路径和相对路径都能使用,但是要知道他们之间有一点差异。通过查看当前目录以及祖先路径(即 ./node_modules, ../node_modules 等等),相对路径将类似于 Node 查找 'node_modules' 的方式进行查找。使用绝对路径,将只在给定目录中搜索。
// webpack.config.js
module.exports = {
//...
resolve: {
modules: ['node_modules'] // 相对路径写法,会按./node_modules, ../node_modules的方式查找
}
};
如果你想要添加一个目录到模块搜索目录,此目录优先于 node_modules/ 搜索:
// webpack.config.js
module.exports = {
//...
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'] // 绝对路径写法
}
};
因此:设置resolve.modules:[path.resolve(__dirname, 'node_modules')]避免层层查找。
(2). resolve.mainFields参考文档: www.webpackjs.com/configurati…
当从 npm包中导入模块时(例如,import * as D3 from "d3"),此选项将决定在 package.json 中使用哪个字段导入模块。根据 webpack 配置中指定的 target 不同,默认值也会有所不同。
当target 属性设置为webworker, web 或者没有指定,默认值为:
module.exports = {
//...
resolve: {
mainFields: ['browser', 'module', 'main']
}
};
对于其他任意的 target(包括 node),默认值为:
module.exports = {
//...
resolve: {
mainFields: ['module', 'main']
}
};
例如,考虑任意一个名为 upstream 的 library,其 package.json包含以下字段
{
"browser": "build/upstream.js",
"module": "index"
}
在我们 import * as Upstream from 'upstream' 时,这实际上会从browser 属性解析文件。在这里 browser属性是最优先选择的,因为它是 mainFields 的第一项。同时,由 webpack 打包的Node.js 应用程序首先会尝试从 module 字段中解析文件。
(3).resolve.alias参考文档:www.webpackjs.com/configurati…
创建 import 或 require 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块:
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
现在,替换「在导入时使用相对路径」这种方式,就像这样:
import Utility from '../../utilities/utility';
你可以这样使用别名:
import Utility from 'Utilities/utility';
也可以在给定对象的键后的末尾添加 $,以表示精准匹配:
module.exports = {
//...
resolve: {
alias: {
xyz$: path.resolve(__dirname, 'path/to/file.js')
}
}
};
这将产生以下结果:
import Test1 from 'xyz'; // 精确匹配,所以 path/to/file.js 被解析和导入
import Test2 from 'xyz/file.js'; // 非精确匹配,触发普通解析
PS: 如果你使用了TS,在webpack中使用了resolve.alias,一般需要在tsconfig.json文件中对其进行配置,否则使用alias会导致无法找到响应目录而报错:
// tsconfig.json
"compilerOptions": {
"paths": {
"@/src/*": ["./src/*"],
'Templates': ["./src/templates/"],
},
}
对庞大的第三方模块设置resolve.alias, 使webpack直接使用库的min文件,避免库内解析
(4). resolve.extensions参考文档:www.webpackjs.com/configurati…
配置resolve.extensions可以自动解析确定的扩展。合理配置resolve.extensions,以减少文件查找
resolve.extensions默认值:extensions:['.wasm', '.mjs', '.js', '.json'],当导入语句没带文件后缀时,Webpack会根据extensions定义的后缀列表进行文件查找,所以:
- 列表值尽量少
- 频率高的文件类型的后缀写在前面
- 源码中的导入语句尽可能的写上文件后缀,如
require(./data)要写成require(./data.json)
常用写法:
extensions: ['.js', '.json', '.ts', '.tsx', '.scss']
2. module.noParse字段告诉Webpack不必解析哪些文件,可以用来排除对非模块化库文件的解析
参考文档:webpack.docschina.org/configurati…
如jQuery、ChartJS,另外如果使用resolve.alias配置了react.min.js,则也应该排除解析,因为react.min.js经过构建,已经是可以直接运行在浏览器的、非模块化的文件了。noParse值可以是RegExp、[RegExp]、function
module:{ noParse:[/jquery|chartjs/, /react\.min\.js$/]}
3. 配置loader时,通过test、exclude、include等缩小搜索范围
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: `/fonts/[name].[hash:8].[ext]`
}
}
二.提升开发效率
开发过程中修改代码后,需要自动构建和刷新浏览器,以查看效果。这个过程可以使用Webpack实现自动化,Webpack负责监听文件的变化,DevServer负责刷新浏览器。
2.1 使用自动刷新
2.1.1 Webpack监听文件
Webpack可以开启监听: 启动webpack时加上--watch参数
// package.json
"scripts": {
"dev": "webpack --watch" // --watch监听打包文件,只要发生变化,就会重新打包。只要有这个参数就生效。
}
但我们想要更丰富的功能:执行npm run dev就会自动打包,并自动打开浏览器,同时可以模拟一些服务器上的特性,此时就要借助WebpackDevServer来实现。
devServer:{
contentBase: './dist' // 服务器起在哪个文件夹下。WebpackDevServer会帮助我们在这个文件夹下起一个服务器
}
配置
devServer:{
port: 8080, // 默认8080
contentBase: './dist',
open: true, // 自动打开浏览器,并访问服务器地址。 file协议不行,不能发送ajax请求
proxy: {
'./api': 'http://localhost:3000' // 用户访问 /api 这个路径会被转发到 http://localhost:3000,支持跨域代理
}
}
2.1.2 DevServer刷新浏览器
devServer: {
contentBase: config.build.assetsRoot,
host: config.dev.host,
port: config.dev.port,
open: true,
inline: true,
hot: true,
overlay: {
warnings: true,
errors: true
},
historyApiFallback: {
rewrites: [
{ from: /^\/index\//, to: `http://${config.dev.host}:${config.dev.port}/index.html` },
]
},
noInfo: true,
disableHostCheck: true,
proxy: {
// '/user/message': {
// target: `http://go.buy.test.mi.com`,
// changeOrigin: true,
// secure: false
// },
}
},
DevServer刷新浏览器有两种方式:
- 向网页中注入代理客户端代码,通过客户端发起刷新
- 向网页装入一个
iframe,通过刷新iframe实现刷新效果
默认情况下,以及 devserver: {inline:true} 都是采用第一种方式刷新页面。第一种方式DevServer因为不知道网页依赖哪些Chunk,所以会向每个chunk中都注入客户端代码,当要输出很多chunk时,会导致构建变慢。而一个页面只需要一个客户端,所以关闭inline模式可以减少构建时间,chunk越多提升越明显。关闭方式:
- 启动时使用
webpack-dev-server --inline false - 配置
devserver:{inline:false}
关闭inline后入口网址变为http://localhost:8080/webpack-dev-server/
另外devServer.compress参数可配置是否采用Gzip压缩,默认为false
2.2 开启模块热替换HMR
模块热替换不刷新整个网页而只重新编译发生变化的模块,并用新模块替换老模块,所以预览反应更快,等待时间更少,同时不刷新页面能保留当前网页的运行状态。原理也是向每一个chunk中注入代理客户端来连接DevServer和网页。开启方式:
webpack-dev-server --hot
使用HotModuleReplacementPlugin,比较麻烦
// package.json
"scripts": {
"start": "webpack-dev-server" ,
}
webpack-dev-server打包后的dist中的内容放到了内存中,加快访问速度
const webpack = require('webpack')
module.exports = {
devServer:{
port: 8080, // 默认8080
contentBase: './dist',
open: true,
hot: true, // 让webpack-dev-server开启Hot Module Replacement功能
hotOnly: true, // 即使HMR功能没有生效,也不让浏览器自动刷新,
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new CleanWebpackPlugin(['dist']), // 开发环境不需要此配置
new webpack.HotModuleReplacementPlugin() // 使用webpack插件,可用于开发环境
],
}
开启后如果修改子模块就可以实现局部刷新,但如果修改的是根JS文件,会整页刷新,原因在于,子模块更新时,事件一层层向上传递,直到某层的文件接收了当前变化的模块,然后执行回调函数。如果一层层向外抛直到最外层都没有文件接收,就会刷新整页。
使用 NamedModulesPlugin 可以使控制台打印出被替换的模块的名称而非数字ID,另外同webpack监听,忽略node_modules目录的文件可以提升性能。
三、优化输出质量-压缩文件体积
3.1 区分环境--减小生产环境代码体积
代码运行环境分为开发环境和生产环境,代码需要根据不同环境做不同的操作,许多第三方库中也有大量的根据开发环境判断的if else代码,构建也需要根据不同环境输出不同的代码,所以需要一套机制可以在源码中区分环境,区分环境之后可以使输出的生产环境的代码体积减小。Webpack中使用DefinePlugin插件来定义配置文件适用的环境。
3.2 压缩代码-JS、CSS
1. 压缩JS:Webpack内置UglifyJS插件、ParallelUglifyPlugin
使用terser-webpack-plugin插件压缩JS代码: 参考文档: webpack.js.org/plugins/ter…
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
safari10: true
}
})
],
}
取代了 UglifyJsPlugin
// 取代 new UglifyJsPlugin(/* ... */)
2. 压缩CSS
2.1 mini-css-extract-plugin:webpack.js.org/plugins/min… 。该插件将CSS提取到单独的文件中。它为每个包含CSS的JS文件创建一个CSS文件。它支持CSS和SourceMap的按需加载。它基于新的webpack v4功能(模块类型)构建,并且需要webpack 4才能正常工作。
2.2 optimize-css-assets-webpack-plugin: www.npmjs.com/package/opt… 。主要是用来压缩css文件
plugins: [
new MiniCssExtractPlugin({
filename: path.join('css/[name].css?[contenthash:8]'),
chunkFilename: path.join('css/[name].chunk.css?[contenthash:8]')
}),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css\?\w*$/
})
],
2.3 cssnano基于PostCSS,不仅是删掉空格,还能理解代码含义,例如把color:#ff0000 转换成 color:red,css-loader内置了cssnano,只需要使用 css-loader?minimize 就可以开启cssnano压缩。
另外一种压缩CSS的方式是使用PurifyCSSPlugin,需要配合 extract-text-webpack-plugin 使用,它主要的作用是可以去除没有用到的CSS代码,类似JS的Tree Shaking。
3.3 使用Tree Shaking剔除JS死代码
参考文档:webpack.docschina.org/guides/tree…
Tree Shaking可以剔除用不上的死代码,它依赖ES6的import、export的模块化语法,最先在Rollup中出现,Webpack 2.0将其引入。适合用于Lodash、utils.js等工具类较分散的文件。它正常工作的前提是代码必须采用ES6的模块化语法,因为ES6模块化语法是静态的(在导入、导出语句中的路径必须是静态字符串,且不能放入其他代码块中)。如果采用了ES5中的模块化,例如module.export = {...}、require( x+y )、if (x) { require( './util' ) },则Webpack无法分析出可以剔除哪些代码。
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015模块语法的 静态结构 特性,例如import和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。
webpack 4正式版本扩展了此检测能力,通过package.json的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
参考文档:webpack.docschina.org/guides/tree…
注意,所有导入文件都会受到tree shaking 的影响。这意味着,如果在项目中使用类似css-loader 并import 一个 CSS 文件,则需要将其添加到side effect列表中,以免在生产模式中无意中将它删除:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
参考文档:webpack.docschina.org/guides/tree…
通过 import 和 export语法,我们已经找出需要删除的“未引用代码(dead code)”,然而,不仅仅是要找出,还要在 bundle 中删除它们。为此,我们需要将 mode配置选项设置为 production。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
- mode: 'development',
- optimization: {
- usedExports: true
- }
+ mode: 'production'
};
注意,也可以在命令行接口中使用 --optimize-minimize 标记,来启用 TerserPlugin。
准备就绪后,然后运行另一个 npm script npm run build,就会看到输出结果发生了改变。
在 dist/bundle.js 中,现在整个 bundle 都已经被 minify(压缩) 和 mangle(混淆破坏),但是如果仔细观察,则不会看到引入 square 函数,但能看到 cube函数的混淆破坏版本(function r(e){return e*e*e}n.a=r)。现在,随着 minification(代码压缩) 和tree shaking,我们的bundle 减小几个字节!虽然,在这个特定示例中,可能看起来没有减少很多,但是,在有着复杂依赖树的大型应用程序上运行 tree shaking时,会对 bundle 产生显著的体积优化。
运行 tree shaking 需要 ModuleConcatenationPlugin。通过 mode: "production" 可以添加此插件。如果你没有使用 mode 设置,记得手动添加 ModuleConcatenationPlugin。
参考文档:webpack.docschina.org/guides/tree…
结论:
我们已经知道,想要使用 tree shaking 必须注意以下几点:
- 使用
ES2015模块语法(即import和export)。 - 确保没有
compiler将ES2015模块语法转换为CommonJS模块(这也是流行的Babel preset中@babel/preset-env的默认行为 - 更多详细信息请查看 文档)。 - 在项目
package.json文件中,添加一个"sideEffects"属性。 - 通过将
mode选项设置为production,启用minification(代码压缩) 和tree shaking。
你可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
四、优化输出质量--加速网络请求
4.1 使用CDN加速静态资源加载
1. CND加速的原理
CDN通过将资源部署到世界各地,使得用户可以就近访问资源,加快访问速度。要接入CDN,需要把网页的静态资源上传到CDN服务上,在访问这些资源时,使用CDN服务提供的URL。
由于CDN会为资源开启长时间的缓存,例如用户从CDN上获取了index.html,即使之后替换了CDN上的index.html,用户那边仍会在使用之前的版本直到缓存时间过期。业界做法:
HTML文件:放在自己的服务器上且关闭缓存,不接入CDN- 静态的
JS、CSS、图片等资源:开启CDN和缓存,同时文件名带上由内容计算出的Hash值,这样只要内容变化hash就会变化,文件名就会变化,就会被重新下载而不论缓存时间多长。
另外,HTTP1.x版本的协议下,浏览器会对于向同一域名并行发起的请求数限制在4~8个。那么把所有静态资源放在同一域名下的CDN服务上就会遇到这种限制,所以可以把他们分散放在不同的CDN服务上,例如JS文件放在js.cdn.com下,将CSS文件放在css.cdn.com下等。这样又会带来一个新的问题:增加了域名解析时间,这个可以通过dns-prefetch来解决 <link rel='dns-prefetch' href='//js.cdn.com'> 来缩减域名解析的时间。形如**//xx.com 这样的URL省略了协议**,这样做的好处是,浏览器在访问资源时会自动根据当前URL采用的模式来决定使用HTTP还是HTTPS协议。
当浏览器从第三方服务跨域请求资源的时候,在浏览器发起请求之前,这个第三方的跨域域名需要被解析为一个IP地址,这个过程就是DNS解析,DNS缓存可以用来减少这个过程的耗时,DNS解析可能会增加请求的延迟,对于那些需要请求许多第三方的资源的网站而言,DNS解析的耗时延迟可能会大大降低网页加载性能。
参考文章: developer.mozilla.org/zh-CN/docs/…
2. 总之,构建需要满足以下几点:
- 静态资源导入的
URL要变成指向CDN服务的绝对路径的URL - 静态资源的文件名需要带上根据内容计算出的
Hash值 - 不同类型资源放在不同域名的
CDN上
3. 最终配置:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, 'dist'),
publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
rules:[{
test: /\.css/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'],
publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
}),
},{
test: /\.png/,
use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值
}]
},
plugins:[
new WebPlugin({
template: './template.html',
filename: 'index.html',
stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
}),
new ExtractTextPlugin({
filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
})
]
4.2 多页面应用提取页面间公共代码,以利用缓存
- 原理
大型网站通常由多个页面组成,每个页面都是一个独立的单页应用,多个页面间肯定会依赖同样的样式文件、技术栈等。如果不把这些公共文件提取出来,那么每个单页打包出来的chunk中都会包含公共代码,相当于要传输n份重复代码。如果把公共文件提取出一个文件,那么当用户访问了一个网页,加载了这个公共文件,再访问其他依赖公共文件的网页时,就直接使用文件在浏览器的缓存,这样公共文件就只用被传输一次。
- 应用方法
把多个页面依赖的公共代码提取到common.js中,此时common.js包含基础库的代码
把多个页面依赖的公共代码提取到common.js中,此时common.js包含基础库的代码
找出依赖的基础库,写一个base.js文件,再与common.js提取公共代码到base中,common.js就剔除了基础库代码,而base.js保持不变
//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
entry:{
base: './base.js'
},
plugins:[
new CommonsChunkPlugin({
chunks:['base','common'],
name:'base',
//minChunks:2, 表示文件要被提取出来需要在指定的chunks中出现的最小次数,防止common.js中没有代码的情况
})
]
- 得到基础库代码
base.js,不含基础库的公共代码common.js,和页面各自的代码文件xx.js。
页面引用顺序如下:base.js--> common.js--> xx.js
4.3 分割代码以按需加载
- 原理
单页应用的一个问题在于使用一个页面承载复杂的功能,要加载的文件体积很大,不进行优化的话会导致首屏加载时间过长,影响用户体验。做按需加载可以解决这个问题。具体方法如下:
- 将网站功能按照相关程度划分成几类
- 每一类合并成一个
Chunk,按需加载对应的Chunk - 例如,只把首屏相关的功能放入执行入口所在的
Chunk,这样首次加载少量的代码,其他代码要用到的时候再去加载。最好提前预估用户接下来的操作,提前加载对应代码,让用户感知不到网络加载
- 做法
一个最简单的例子:网页首次只加载main.js,网页展示一个按钮,点击按钮时加载分割出去的show.js,加载成功后执行show.js里的函数
//main.js
document.getElementById('btn').addEventListener('click',function(){
import(/* webpackChunkName:"show" */ './show').then((show)=>{
show('Webpack');
})
})
//show.js
module.exports = function (content) {
window.alert('Hello ' + content);
}
import(/* webpackChunkName:show */ './show').then() 是实现按需加载的关键,Webpack内置对import( *)语句的支持,Webpack会以./show.js为入口重新生成一个Chunk。代码在浏览器上运行时只有点击了按钮才会开始加载show.js,且import语句会返回一个Promise,加载成功后可以在then方法中获取加载的内容。这要求浏览器支持Promise API,对于不支持的浏览器,需要注入Promise polyfill。/* webpackChunkName:show */ 是定义动态生成的Chunk的名称,默认名称是[id].js,定义名称方便调试代码。为了正确输出这个配置的ChunkName,还需要配置Webpack:
//...
output:{
filename:'[name].js',
chunkFilename:'[name].js', // 指定动态生成的Chunk在输出时的文件名称
}
五、优化输出质量--提升代码运行时的效率
5.1 使用Prepack提前求值
- 原理:
Prepack是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。通过在便一阶段预先执行源码来得到执行结果,再直接将运行结果输出以提升性能。但是现在Prepack还不够成熟,用于线上环境还为时过早。
- 使用方法
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
module.exports = {
plugins:[
new PrepackWebpackPlugin()
]
}
5.2 使用Scope Hoisting
- 原理
译作“作用域提升”,是在Webpack3中推出的功能,它分析模块间的依赖关系,尽可能将被打散的模块合并到一个函数中,但不能造成代码冗余,所以只有被引用一次的模块才能被合并。由于需要分析模块间的依赖关系,所以源码必须是采用了ES6模块化的,否则Webpack会降级处理不采用Scope Hoisting。
- 使用方法
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
//...
plugins:[
new ModuleConcatenationPlugin();
],
resolve:{
mainFields:['jsnext:main','browser','main']
}
webpack --display-optimization-bailout 输出日志中会提示哪个文件导致了降级处理
六、使用输出分析工具
启动Webpack时带上这两个参数可以生成一个json文件,输出分析工具大多依赖该文件进行分析:
webpack --profile --json > stats.json 其中 --profile 记录构建过程中的耗时信息,--json 以JSON的格式输出构建结果,>stats.json 是UNIX / Linux系统中的管道命令,含义是将内容通过管道输出到stats.json文件中。
- 官方工具
Webpack Analyse
打开该工具的官网http://webpack.github.io/analyse/上传stats.json,就可以得到分析结果
webpack-bundle-analyzer
可视化分析工具,比Webapck Analyse更直观。使用也很简单:
npm i -g webpack-bundle-analyzer安装到全局
按照上面方法生成stats.json文件
在项目根目录执行webpack-bundle-analyzer,浏览器会自动打开结果分析页面。