区分 Develoment 和 Production
- 我们配置
webpack的时候都会分 开发(dev) 和 生产环境(prod),可以粗浅理解它们的区别就是会不会压缩代码,我们可以对应做不同的处理 - 总之,我们需要在不同的环境进行不同的配置
分开配置文件
- 我们可以把 开发 和 生产 环境分别配置两个文件,通过指定
config文件区分 - 但是配置总会有很多相同的,我们要把这些相同的东西拿出来,最后再与环境独有的合并
- 使用 webpack-merge 进行合并
// webpack.dev.js 开发
// webpack.prod.js 生产
// webpack.common.js 公共
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {}
module.exports = merge(commonConfig, devConfig);
使用环境变量
- 我们也可以通过传入环境变量
env和node-env去对配置文件进行区分
:: --env prod
:: --node-env production
npx webpack --env prod --node-env production
- 导出改为函数
module.exports = env => {
console.log(env.prod); // true
console.log(process.env.NODE_ENV); // 'production'
}
- 注意:除了传
prod之外,其实我们还能传不同的东西;规则:env
DefinePlugin
webpack内置了一个插件DefinePlugin,它允许创建一个在编译时可以配置的全局常量,通过这个插件,我们也可以在开发过程中得知我们所在的环境
new webpack.DefinePlugin({
NODE_ENV: process.env.NODE_ENV
})
Tree Shaking
概念
Tree Shaking- 树摇,这名字很贴切,就是把我们代码树中,不需要的叶子摇出来- 想象一个场景,一个模块导出 n 个方法,另一个模块引入其中一个方法,我们打包代码的时候肯定是希望只打包我们引入的这个方法,而不是整个模块
Tree Shaking指的就是当我们引入一个模块的时候,不引入这个模块的所有代码,只引入我需要的代码- 它并不只是在
webpack独有,它的本质是消除无用的 js 代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination) - 注意:在
webpack内Tree Shaking只支持ES Module,因为在webpack中,ESM是静态引入,CommonJS是动态引入(可变)
使用
- 只有在
production下,Tree Shaking才会真正被使用 - 在
development下,Tree Shaking还是可以打开的,虽然不能直观看到代码量减少,但是可以通过webpack的备注看到哪些模块是未使用的
module.exports = {
optimization: {
usedExports: true
}
}
sideEffects & 副作用
webpack还未能完全识别出所有的副作用,这会在打包时产生一些无谓的代码sideEffects允许通过配置的方式去标识代码是否有副作用,从而为Tree Shaking提供更多的压缩空间
副作用
webpack是一个模块打包工具,但是模块除了导出和导入之外,还可以写其他的东西,例如在window下绑定某些方法,或是一些行为,这些被称为副作用,典型例子就是polyfill
export const fn = () => {
console.log('fn');
}
// 下面这些就被称为副作用
window.fn = () => {
// do something
}
console.log('副作用');
document.documentElement.append('副作用')
package.json[sideEffects]
-
webpack会读取package.json内sideEffects字段,它提供关于代码纯度的提示true:默认,所有代码都有可能存在副作用,webpack会通过 terser 检测语句中的副作用false:所有代码均无副作用Array:提供有副作用的文件或目录
{ "sideEffects": [ "*.css", // 所有 css 文件 "./src/effect.js", // 指定文件 "polyfill/", // 指定目录 ] } -
注意:不光是
js;css等模块也是由sideEffects决定副作用去留,因为css都是副作用,请务必加上
optimization[sideEffects]
- 因为是同名,而且两者有关系,千万不要搞混了,在
optimization里,它的意思是是否去除副作用true:默认 - 去除副作用false:保留所有副作用
效果
- 这里的设置为
package.json[sideEffects]=falseoptimization[sideEffects]=true
// a.js
export default 123;
console.log(123);
// b.js
window.fn = () => { };
// c.js
export const fn = () => { };
console.log(456);
// index.js - 执行文件
import a from './a'; // 引入未使用,不保留副作用
import './b'; // 引入无导出,不保留副作用
// 引入使用,保留副作用
import { fn } from './c';
fn();
- 由此可见,以下场景如使用
sideEffects,请务必加上标识- 立即执行的脚本 - 包括改变 DOM,全局变量
css模块- 等等...
Code Splitting
-
默认情况下,
webpack会将所有引入的模块都打包到一个文件中,这样导致了打包后的文件比较大 -
例如我们引入
@babel/polyfill,假设1MB,然后自己业务代码又有1MB,打包出来的结果就有2MB,首先就会有两个问题- 打包文件过大,加载时间太长
- 修改一点点代码,用户又要重新访问又要加载
2MB的文件
-
我们聪明的朋友就会想,那我把一些代码挂载在
window上面,然后设置多入口,这不就成了嘛
// polyfill
import '@babel/polyfill';
// lodash
import _ from 'lodash';
window['_'] = _;
// jQuery
import $ from 'jquery';
window['$'] = $;
...
// index.js
console.log('业务代码');
module.exports = {
entry: {
polyfill: './src/polyfill.js', // 1mb
lodash: './src/lodash.js', // 1mb
jQuery: './src/jQuery.js', // 1mb
main: './src/index.js', // 1mb
}
}
- 当然这不是不可以,不过过于麻烦,不智能
SplitChunksPlugin
- 以上这都是代码分割的问题,我们需要把代码分成不同的
chunks,而webpack本身自带了一个插件做这样的事情 SplitChunksPlugin - 最简单的使用就是在
optimization加上splitChunks,它会以默认参数执行分割
默认配置
module.exports = {
optimization: {
splitChunks: {
chunks: "async",
minSize: 30000, // 5.x 为 20000
minChunks: 1,
maxAsyncRequests: 5, // 5.x 为 30
maxInitialRequests: 3, // 5.x 为 30
automaticNameDelimiter: '~',
name: true, // 5.x 为 false
minRemainingSize: 0,
enforceSizeThreshold: 50000,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
配置参数(大部分)
chunks
- 将选择哪些 chunks 进行优化
async:异步initial:同步all:同步异步Function:自定义包含哪个 chunks
module.exports = {
optimization: {
splitChunks: {
chunks(chunk) {
// exclude `my-excluded-chunk`
return chunk.name !== 'my-excluded-chunk';
},
},
},
};
minSize
- 大于
minSize设置的大小才进行分割 - 单位:字节
maxSize
- 尝试将大于
maxSize字节的 chunks 拆分为更小的部分
hidePathInfo
- 在为按
maxSize分割的部分创建名称时防止暴露路径信息
minChunks
- 至少使用了
minChunks次的模块才进行分割
maxAsyncRequests
- 最多同时加载的模块数量(并行请求数)
- 如果超过,将不再进行分割
maxInitialRequests
- 入口文件,最多同时加载的模块数量(并行请求数)
minRemainingSize
webpack 5.x独有,仅在剩余一个chunk时生效,限制拆分后剩余的chunk的最小大小,避免了0大小的模块
enforceSizeThreshold
- 超过阈值将忽略
minRemainingSizemaxAsyncRequestsmaxInitialRequests进行强制拆分
automaticNameDelimiter
chunks命名连接符,将来源和名称进行连接- 例如:
vendors~main.js-vendors缓存组~来源于main文件(入口)
name
chunks名称,也可以用在cacheGroups内false:默认名称 -webpack推荐值true:采用 缓存~来源 方式命名,webpack 5.x被干掉了Function:自定义名称- 注意:使用自定义名称,会根据命名不同生成多个文件(可做到极细分割 chunk)
- 注意:不同的
chunks有相同名称时,均会放置在一个共享块内 - 注意:如果与入口文件命名相同,入口文件将被删除
splitChunks: {
name(module, chunks, cacheGroupKey) {
// const absolutePath = module.identifier(); // 模块标识符
const absolutePath = module.resource; // 资源路径
const moduleFileName = path.basename(absolutePath, path.extname(absolutePath)); // 文件名
const allChunksNames = chunks.map((item) => {
return item.name
}).join('~'); // 所有 chunk 来源(入口)
// cacheGroupKey // 缓存组 key
return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
}
}
cacheGroups
- 缓存组,其实相当于一个分组条件,正常情况下满足这些条件则输出为一个
chunks(name的不同可分割为多个) - 它可以继承
splitChunks的其他所有配置,也可以覆盖,但是自己也有特有配置 - 如果禁用缓存组,设置为
false
splitChunks: {
cacheGroups: {
default: false,
},
},
priority
- 优先级
- 一个模块可以符合多个缓存组的条件,将优先选择高的
priority - 默认组优先级为
-20,自定义默认组有限制默认为0
reuseExistingChunk
true- 如果引入模块包含已经被分离出来的模块,将会服用,而不会重新生成一个新的
// a
import b from './b'
// index
import a from './a'
import b from './b' // 不会重新生成,服用 a 引入的 b
type
webpack 5.x独有,按模块类型分配缓存组function (type)=>booleanRegExpstring
test
- 控制缓存组选择哪些模块,不写会选择所有模块,就是大家都熟悉的那个
test匹配 function (module, { chunkGraph, moduleGraph }) => booleanRegExpstring
Dynamic Imports 动态引入
webpack支持两种方式 - import() 和 require.ensure 动态加载模块- 强烈推荐
ECMA提议的import()而非webpack独有的require.ensure(官方也是这么说的),下面只涉及import() - 使用动态引入时,它将会被视为
Code Splitting分割点 - 请求的模块和子模块会被分割成一个独立的chunks - 注意:
SplitChunksPlugin[chunks] != 'initial'时,会被cacheGroups等配置影响行为,但绝对会进行分割
简单使用
- 调用
import()会返回一个Promise,接下来就是熟悉的then或者async await
// default 是因为 CommonJS 的兼容
import('lodash').then(({ default: _ }) => {
console.log(_);
});
- 注意:
import()依赖特性Promise,如果要兼容旧浏览器记得使用polyfill
动态表达式
import()可以使用动态语句导入,但是!!不可以只传入一个变量,例如import(foo)- 使用
import()必须包含一些关于模块所在位置的信息,限制在特定目录
const _module = 'a';
import(`../abc/${a}`).then(({ default: res }) => {
console.log(res);
})
Magic Comments 魔法注释
-
在使用
import()时,可以加入一些神奇的内联注释,webpack会在打包时根据这些注释做不同的处理 -
webpackChunkName:新chunks的名称,占位符[index]和[request]对应的分别是 递增的数字 和 实际解析的文件名,此注释将让我们打包的文件有自定义名称,我们也可以用这个注释将不同的chunk打包到同一个chunks中 -
webpackMode:可以指定不同的解析动态导入模式lazy:默认,为每个模块生成一个可延迟加载的chunkeager:不生成额外的chunks,不会发出额外的网络请求,但依然由Promise返回weak:永远不会执行网络请求,以Promise返回,如该模块以其他方式加载,尝试加载模块返回,否则返回rejected
-
webpackInclude:在使用 动态语句导入 时,解析只有匹配到模块才会打包 -
webpackExclude:在使用 动态语句导入 时,所有匹配到的模块都不会被打包
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
`./locale/${language}`
);
Prefetching/Preloading modules
Prefetching
- 在
SplitChunksPlugin的默认配置我们可以看到chunks参数的默认值是async而并非all,有没有一种可能,webpack更希望我们通过异步的方式进行代码分割? - 同步引入的代码分割成一个文件后,运行时在第二次进入页面时,我们可以借助缓存提高页面访问速度,但我们是不是可以再次优化,将一些需要交互之后才会起作用的非必要代码,进行异步导入,从而提高第一次进入页面的速度呢?这正是
webpack提倡的 - 我们可以通过浏览器的
Coverage功能看到网页代码使用率,快速搜索出网页未用的代码进行优化 - 但这也产生一个问题,在进行异步加载的过程中,很可能会因为网速问题,导致加载过慢导致交互卡顿;我们可以利用网络空闲时进行加载,从而提高响应速度,我们可以使用
webpackPrefetch做到这一点
import(/* webpackPrefetch: true */ './popup');
- 它实际上往
<head>上添加了<link rel="prefetch">,让浏览器在闲置时间预取
Preloading
Preload的作用并非是为了不确定使用的资源而使用的,而是当前模块下必须使用的资源- 因为动态获取资源对浏览器预加载器并不可见,这严重影响了它们在资源获取队列中的优先级,而
Preload的作用就是让预加载器知道这个动态请求文件的存在,chrome甚至会在资源加载后3秒没有被使用时打印一个警告 Preload与 父级文件并行加载
import(/* webpackPreload: true */ './index.scss');
- 它实际上往
<head>上添加了<link rel="preload">
联合举例
// main.js
document.documentElement.onclick = () => {
import(/* webpackPrefetch: true */ `./a`).then(({ fn }) => {
setTimeout(() => {
fn();
}, 4000);
})
}
// ./a.js
alert(1)
export const fn = () => {
import(/* webpackPreload: true */ `./b`).then(res => {
console.log(res);
})
}
- 本地环境,请求完成不超过
1s,执行顺序如下
- 加载
main.js main.js加载完成后;往<head>添加<link rel="prefetch" as="script" href="/a.js">进行预取- 点击
document,缓存读取a.js <head>添加<link charset="utf-8" rel="preload" as="script" href="/b.js">,告知浏览器获取资源,浏览器请求b.jsalert(1)b.js请求过后3s,chrome警告未使用资源
- 注意:
a.js不使用prefetch时,a.jsb.js并行加载 - 注意:
b.js不使用preload时,a.js中会先执行alert再进行b.js的请求
打包分析
- 在
webpack进行打包之后,会在控制台简单输出打包信息,但这往往看起来很麻烦,也不够直观,我们可以通过一些打包分析的工具直观详细的看到打包信息,检查打包是否合理
生成 stats.json
- 很多分析工具都需要我们提供打包时的信息,我们要生成一个
json文件 - 加上
--profile --json > stats.json,运行webpack会生成包含打包信息文件,我们也可以直接从这个文件看到打包信息
"scripts": {
"build": "webpack --profile --json > stats.json"
},
工具
- 官方分析工具
- webpack bundle optimize helper:这个工具会分析你的 bundle 并为你提供可行的建议,告诉你如何改进以减少你的 bundle 大小
- webpack-bundle-analyzer:插件 | CLI - 直观的图形,方便的交互,缩放树形图
- ...
缓存(Caching) 与 hash
-
性能优化的方式中,缓存非常重要,但它也会对我们的及时更新产生影响,往往我们需要手动更新版本号进行处理
-
webpack给我们内置了hash,我们可以通过它很好的解决这个问题 -
webpack在编译时产生的hash有三种hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,所有文件共用一个hashchunkhash:根据不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的hashcontenthash:根据文件内容生成不同的hash,只有当文件内容发生改变时才会重新生成hash
-
hash和chunkhash在每次打包后都会生成不同的值,没办法利用于处理缓存问题,而contenthash正正合适
module.exports = {
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
},
}
[contenthash]并非只在output存在,file-loaderminiCssExtractPlugin等等都可以利用它帮助我们自动解决缓存问题- 老版本的
webpack(大概 4 以下) 因为manifest(用于管理所有模块之间的运行关系) 处理的差异,可能有在内容未改变但contenthash改变了的问题,可以通过加上runtimeChunk解决
optimization: {
runtimeChunk: {
name: 'runtime'
}
}
Shimming
- 一些旧的第三方库可能并非遵循现在的模块编写规范,它们可能需要一些全局依赖,又或者
this有特殊指向,而非模块自身,对于这些 “不符合规范的模块”,可以使用Shimming对它们进行处理
ProvidePlugin
- 自动加载模块
- 简单来说它帮我们把缺少的模块引入补齐(非全局),从而使
library正常运行 - 还能暴露某个模块中单个导出值
// index.js
// import $ from 'jquery'
console.log($);
// import lodash from 'lodash';
// console.log(lodash.join(['hello', 'ProvidePlugin'], ''));
console.log(_join(['hello', 'ProvidePlugin'], ''));
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
_join: ['lodash', 'join']
}),
]
}
imports-loader
- 一些库
this指向window,可以使用imports-loader改变它 - 当然这个
loader也可以干ProvidePlugin的活,这里不阐述,有兴趣可以瞄瞄文档
npm install imports-loader -D
module.exports = {
module: {
rules: [{
test: path.resolve('./src', 'index.js'),
// use: 'imports-loader?this=>window', // webpack 4.x
use: 'imports-loader?wrapper=window' // webpack 5.x
}]
}
}
externals - 外部扩展
- 它与
Shimming可以说是相反的,Shimming是补上import,而externals则是我已经import时,需要把他改为 CDN 引入(全局) - 防止
import的模块打包进bundle,而是运行时再去外部获取
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
// index.js
import $ from 'jquery';
console.log($);
// webpack.config.js
module.exports = {
externals: {
$: 'jQuery'
},
}