1. 代码分割
- 针对大型 Web 应用来讲,将所有的代码都打包成一个文件显然是不够有效的,特别是当某些代码块是在某些特殊的时候才会被用到。
- webpack5 能够将代码库分割成 chunks 语块,当代码运行到需要它们的时候再进行加载。
2. 入口点分割
- Entry Points:入口文件设置的时候可以配置,就像多入口配置,entry 配置成对象形式
- 该方式存在问题
- 如果入口 chunks 之间包含重复的模块 (比如:lodash),那些重复模块都会被引入到各个 bundle 中
- 不够灵活,并且不能将核心应用程序代码逻辑进行动态拆分出来
webpack.config.js
const path = require("path");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "production",
+ entry: {
+ one: "./src/one.js",
+ two: "./src/two.js",
+ vendor: ["jquery", "lodash", "other-lib"],
+ },
context: process.cwd(),
output: {
path: path.resolve("dist"),
filename: "[name].[chunkhash:8].js",
},
plugins: [
new webpack.ProgressPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src/index.html"),
}),
],
};
3. 动态导入和懒加载
- 用户当前需要用什么功能就只加载该功能的对应代码段,即 按需加载 。
- 给单页应用做按需加载优化时,一般采用以下原则:
- 对网站功能进行划分,每一功能一个 chunk。
- 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载。
- 被分割出去的代码需要一个按需加载的时机。
src/index.html
+ <button id="btn">click</button>
src/index.js
const oBtn = document.querySelector("button#btn");
oBtn.addEventListener("click", async () => {
// import 就会按需加载了,只有在点击之后,才加载 hello.js 的代码内容
const result = await import("./hello.js");
console.log("result: ", result);
});
PS: 通过魔法注释改变打包后的 chunkName,import("./hello" /* webpackChunkName: "hello" */);
3.1 preload
- preload 通常用于本页面要用到的关键资源,包括关键js、字体、css文件的加载
- preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度
- 在资源上添加预先加载的注释,你指明该模块需要立即被使用
- 一个资源的加载的优先级被分为五个级别,分别是
- Highest 最高
- High 高
- Medium 中等
- Low 低
- Lowest 最低
- 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是
Low
PS:谨慎使用 preload
实现简易 preload
src/index.html
+ <button id="btn">click</button>
src/index.js
const oBtn = document.querySelector("button#btn");
oBtn.addEventListener("click", async () => {
// webpack 源码中 4 年前已经删除该功能/* webpackPreload: true */的解析: https://github.com/webpack/webpack/pull/7056#event-1578278014 https://github.com/webpack/webpack/pull/7056
+ const result = await import(/* webpackPreload: true, webpackChunkName: "aaa" */ "./hello");
console.log("result: ", result);
});
PS:生成
<link rel="preload" as="script" href="http://localhost:9999/395.9e689745.js">
const HtmlWebpackPlugin = require("html-webpack-plugin");
/**
* 1. 找到异步加载的代码块
* 2. 对每个异步的代码块,生成 <link rel="preload" as="script" href="http://localhost:9999/395.9e689745.js">
* 3. 把这个 link 的标签添加到 html 页面
*/
class PreloadWebpackPlugin {
constructor(options) {
this.options = options || {};
this.preloadSources = new Set();
}
apply(compiler) {
compiler.hooks.compilation.tap(
"RequireContextPlugin",
(compilation, { contextModuleFactory, normalModuleFactory }) => {
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("RequireContextPlugin", (parser, parserOptions) => {
parser.hooks.importCall.tap("ImportParserPlugin", (expr) => {
const { options: importOptions } = parser.parseCommentOptions(
expr.range
);
// import(/* webpackPreload: true */ "./hello"),读取到解析后的 webpackPreload 值
if (importOptions && importOptions.webpackPreload) {
const source = expr.source.value;
this.preloadSources.add(source);
// source 是 import("./hello") 这了的 ./hello
console.log("source", source);
// 设置为 preload
this.preload = "preload";
}
});
});
}
);
compiler.hooks.compilation.tap("PreloadWebpackPlugin", (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"PreloadWebpackPlugin",
(html, callback) => {
this.generateLinks(compilation, html, callback);
}
);
});
compiler.hooks.compilation.tap("PreloadWebpackPlugin", (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tap(
"PreloadWebpackPlugin",
(html) => {
const { resourceHints } = this;
if (resourceHints) {
html.assetTags.styles = [
...resourceHints,
...html.assetTags.styles,
];
}
return html;
}
);
});
}
generateLinks(compilation, html, callback) {
const { rel, include } = this.options;
let chunks = [...compilation.chunks];
if (include === undefined || include === "asyncChunks") {
// chunk.canBeInitial() 返回 true 是入口代码块
chunks = chunks
.filter((chunk) => !chunk.canBeInitial())
.filter((chunk) => {
// chunk.name 是 webpackChunkName : import(/* webpackPreload: true, webpackChunkName: "aaa" */ "./hello")
const module = chunk.modulesIterable.values();
const { value } = module.next();
// 获取到 ./hello 是 import("./helle") 括号中的
const rawRequest = value.rawRequest;
return this.preloadSources.has(rawRequest);
}); // 留下异步代码块
}
const allFiles = chunks.reduce((set, chunk) => {
return set.add(...chunk.files);
}, new Set());
const links = [];
for (const file of allFiles.values()) {
links.push({
tagName: "link",
attributes: {
rel: this.preload, // preload prefetch
href: file,
as: "script",
},
});
}
this.resourceHints = links;
callback();
}
}
module.exports = PreloadWebpackPlugin;
3.2 prefetch
-
prefetch 跟 preload 不同,作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
-
通过魔法注释预加载
import("./hello" /* webpackPrefetch: true */);
-
给 link 标签 添加
rel="prefetch"
<link rel="prefetch" as="script" href="http://localhost:9999/395.9e689745.js">
-
3.3 preload vs prefetch
- preload 告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
- 而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。
建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch。
4. 提取公共代码
4.1 提取原则
- 提取基础类库,便于长期缓存。
- 提取页面之间的公用代码。
- 提取出各个页面单独生成文件。
4.2 module、chunk、bundle
- module:是 javascript 的模块化,webpack 支持 commonJS、ES6 等模块化规范,简单来说就是你通过import 语句引入的代码。
- chunk: chunk 是 webpack 根据功能拆分出来的,包含三种情况
- 项目入口(entry)
- 通过 import() 动态引入的代码
- 通过 splitChunks 拆分出来的代码
- bundle:bundle 是 webpack 打包之后的各个文件,一般就是和 chunk 是一对一的关系,bundle 就是对chunk 进行编译压缩打包等处理之后的产出。
4.3 webpack.config.js
const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "production",
entry: {
page1: "./src/page1.js",
page2: "./src/page2.js",
page3: "./src/page3.js",
},
context: process.cwd(),
output: {
path: path.resolve("dist"),
filename: "[name].[chunkhash:8].js",
},
optimization: {
splitChunks: {
// 表示选择哪些 chunks 进行分割,可选值有:async,initial 和 all
chunks: "all",
// 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
minSize: 0, // 大于该值就需要分割出去
// 表示一个模块至少应被 minChunks 个 chunk 所包含才能分割。默认为1。
minChunks: 1, // 大于该值就需要分割出去,被几个代码块导入表示的次数
// 表示按需加载文件时,并行请求的最大数目。默认为5。
maxAsyncRequests: 3,
// 表示加载入口文件时,并行请求的最大数目。默认为3
maxInitialRequests: 5,
// 表示拆分出的chunk的名称连接符。默认为~。如 chunk~vendors.js
automaticNameDelimiter: "~",
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 优先级,一个 chunk 很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
default: {
minChunks: 2, ////被多少模块共享,在分割之前模块的被引用次数
priority: -20,
},
},
},
runtimeChunk: true, // 为 true 或 'multiple',会为每个入口添加一个只含有 runtime 的额外 chunk
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src/index.html"),
chunks: ["page1"], // entry 入口配置的属性
filename: "page1.html",
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src/index.html"),
chunks: ["page2"], // entry 入口配置的属性
filename: "page2.html",
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src/index.html"),
chunks: ["page3"], // entry 入口配置的属性
filename: "page3.html",
}),
],
};
src/page1.js
const module1 = require('./module1');
const module2 = require('./module2');
const $ = require('jquery');
console.log(module1, module2, $);
import( /* webpackChunkName: "asyncModule1" */ './asyncModule1');
src/page2.js
const module1 = require('./module1');
const module2 = require('./module2');
const $ = require('jquery');
console.log(module1, module2, $);
src/page3.js
const module1 = require('./module1');
const module3 = require('./module3');
const $ = require('jquery');
console.log(module1, module3, $);
src/module1.js
module.exports = 'module1';
src/module2.js
module.exports = 'module2';
src/module3.js
module.exports = 'module3';
src/asyncModule1.js
import _ from 'lodash';
console.log(_);
结果:
// 入口代码块
page1.js
page2.js
page3.js
// 异步加载代码块
src_asyncModule1_js.js
// defaultVendors 缓存组对应的代码块
defaultVendors-node_modules_jquery_dist_jquery_js.js
defaultVendors-node_modules_lodash_lodash_js.js
// default 代缓存组对应的代码块
default-src_module1_js.js
default-src_module2_js.js
// runtimeChunk: true
runtime~page1.js
runtime~pag2.js
runtime~page3.js