Webpack 的 Chunk,想怎么分就怎么分

article/2025/8/27 20:42:38

想必大家都用过 webpack,也或多或少了解它的原理,但是不知道大家有没有写过 Webpack 的插件呢?

今天我们就一起来写一个划分 Chunk 的 webpack 插件吧,写完后你会发现想怎么分 Chunk 都可以!

首先我们简单了解下 webpack 的原理:

webpack 的原理

webpack 是一个打包工具(bundler),它打包的是什么呢?

模块。

那模块能再拆分么?

不能了,模块是 webpack 处理的基本单位了,只是对模块做一些打包。

那怎么对模块打包呢?

首先要找到所有的模块(Module),从入口模块开始,分析依赖,构成一个图,叫做模块依赖图(ModuleGraph)。

然后模块要分成几个包,要有一种中间结构来保存这种划分,叫做 Chunk。把不同的模块放到不同的 Chunk 里就完成了分包。

但是 Chunk 只是一种中间结构,还要再变成可用的目标代码。通过代码模版把它打印成代码就可以了。

这三步分别叫 make、seal、emit。

make 这一步就是构建模块依赖图 ModuleGraph 的,这个过程中会从入口模块(EntryPoint)开始递归解析依赖,对解析出的每个模块做处理,也就是调用注册的 loader。

然后 Seal 也就是封装的意思,把不同的 Module 分到不同的 Chunk 里。

这一步会先做基础的 Chunk 划分,比如入口模块 EntryPoint 肯定要单独放到 Chunk 里,动态引入的模块肯定也要单独放到 Chunk 里。

完成了基础的划分之后,可以再对这些 Chunk 做进一步的优化划分,比如根据 Chunk 大小等来划分。

分完之后,ModuleGrapqh 就变成了 ChunkGraph。

最后 emit 阶段就是通过模版打印代码了。

这三步合起来就是一次编译过程 Compilation。

编译过程由 webpack 的 Compiler 调用。

整个过程中还会暴露出很多扩展点,也就是留给插件的 hook,不同阶段的 hook 自然就可以拿到不同阶段的资源。

这些插件都是保存在对象上的:

比如 compiler 的 hook:

compilation 的 hook:

那插件里自然就是往不同对象的 hook 上添加回调函数:

而且 webpack 为了控制 hook 的执行顺序,封装了一个 tappable 的包。可以指定 hook 是同步、异步,并行、串行执行。

比如这几种 hook:

SynHook 就是同步顺序执行。

AsyncSeriesHook 就是异步串行执行。

SyncBailHook 也是同步顺序执行,但是如果中间的 hook 返回 false 就会停止后续 hook 的执行,也就是可以熔断。

理解了 webpack 的编译流程,hook 的运行机制,接下来我们就写个插件来操作下 Chunk 吧:

操作 Chunk 的 webpack 插件

前面讲过,webpack 会对 Module 根据是否是入口模块、是否是异步引入的模块做基础的 Chunk 划分。

之后会进一步做优化的 Chunk 划分。

这些 chunk 相关的逻辑都是在 seal 那一步做的。

我们在源码里看到的也确实是这样:

在 seal 里做了 ChunkGraph 的创建,然后调用 optimizeChunks 的 hook 对 Chunks 做处理。

这里为啥是个死循环呢?

记得上面说过一种 hook 类型叫 SyncBailHook 么?

也就是同步执行插件,但是可以插件可以返回 false 熔断后面插件的执行。

这里的 hook 就是同步熔断 hook:

那我们就开始在这个 hook 里写一些逻辑吧:

class ChunkTestPlugin {constructor(options) {this.options = options || {};}apply(compiler) {const options = this.options;compiler.hooks.thisCompilation.tap("ChunkTestPlugin", compilation => {compilation.hooks.optimizeChunks.tap("ChunkTestPlugin", chunks => {return true;});});}
}module.exports = ChunkTestPlugin; 

把 options 挂到 this 上。

然后注册一个 optimizeChunks 这个 hook 的回调。

为啥外面还要加一层 compiler 的 hook 呢?

因为你得在 compiler 刚开始编译的时候去注册 compilation 的 hook 呀!不然就晚了。

可以看到 thisCompilation 是在 newCompilation 这个方法调用的。

而 newCompilation 是在 make、seal、emit 的流程开始之前调用的:

也就是说在 thisCompilation 的 Compiler hook 里注册的 Compilation hook 就可以在这次编译过程中生效。

有的同学说,那还有另一个 hook 是干啥的呢?

这俩 hook 唯一的区别是当有 child compiler 的时候,compilation 的 hook 会生效,而 thisCompilation 不会。

而我们是想在这个 hook 里注册 Compilation 的 hook 的,全局只需要执行一次就行,所以用 thisCompilation 的 Compiler hook。

我们在项目里用一下:

这个项目有三个入口模块:

pageA:

require(["./common"], function (common) {common(require("./a"));
}); 

pageB:

require(["./common"], function(common) {common(require("./b"));
}); 

pageC:

require(["./a"], function(a) {console.log(a + require("./b"));
}); 

这三个模块里都通过 requrie() 或者 import() 的 webpack api 来动态引入了一些模块。

动态引入的模块分别是:

a:

module.exports = "a"; 

b:

module.exports = "b"; 

common:

module.exports = function (msg) {console.log(msg);
};function hello() {return "guangguangguangguangguangguanggua"+ "ngguangguangguangguangguangguangguangguangg"+ "uangguangguangguangguangguangguangguangguangg"+ "uangguangguangguangguangguangguangguangguang"+ "guangguangguangguangguangguangguangguangguangguang"+ "guangguangguangguangguangguangguangguangguangguangguang";
} 

webpack 会从入口模块开始构建 ModuleGraph,然后划分 Chunk,构成 ChunkGraph。

大家觉得这几个模块会分几个 Chunk 呢?

6 个。

因为入口模块要用单独的 Chunk,而且异步引入的模块也是单独的 Chunk。

打个断点看一下就知道了:

确实,到了 optimizeChunks 这一步,拿到的是 6 个 Chunk:

分别是 3 个入口,以及每个入口用到的异步模块。

在这个 optimizeHook 的插件里,我们就可以自己做一些 Chunk 拆分了。

chunkGroup 有一个 integrateChunks 的 api,把后面的 chunk 合并到前面的 chunk 里:

我们调用 integrateChunks 进行 chunk 合并,然后把被合并的那个 chunk 删掉即可。

那怎么找到 a 和 b 两个 chunk 呢?

两层循环,分别找到两个不想等的 chunk 进行合并即可:

我们只取第一组 chunk 进行合并,合并完如果还有就返回 true,继续进行下次合并。

合并完之后记得 return false,因为外面是一个 while 循环,不 return false,就一直死循环。

先试一下现在的效果:

不引入插件的时候是这样的:

3 个入口 chunk,3 组入口 chunk 的异步引入的模块。所以产生了 6 个文件。

入口 chunk 对应的文件里引入异步模块的方法变成了 webpack runtime 的 _webpack_require.e

而它引入的异步 chunk 里就如前面分析的,包含了这个模块的所有异步依赖:

分别是 a + common,b + common,a + b,也就是每个入口模块依赖的所有异步模块。

那优化之后呢?

都放到一个 chunk 里了:

这倒是符合我们写的逻辑,因为两两合并,最后剩下的肯定只有一个。

但这样显然不大好,因为每个页面是独立的,应该分开,但是异步的 chunk 倒是可以合并。

所以我们优化一下:

调用 chunk 的 isInitial 方法就可以判断是否是入口的 chunk,是的话就跳过。

这样就只合并了异步 chunk。

效果是这样的:

3 个入口 chunk 的依赖也变成这个 chunk 了:

那如果我要根据 chunk 大小来优化呢?

那就可以判断下 a、b 的 chunk 的大小和合并之后的 chunk 大小,如果合并之后比合并前小很多,就合并。

当然,不同的 chunk 合并效果是不一样的,我们要把所有的合并效果下来:

通过 chunkGraph.getChunkSize 的 api 拿到 chunk 大小,通过 chunkGroup.getIntegratedChunkSize 的 api 拿到合并后的 chunk 大小。

记录下合并的两个 chunk 合并的收益。

做个排序,把合并收益最大的两个 chunk 合并。

返回 true 来继续循环进行合并,直到收益小于 1.5,那就 return false 停止合并。

当然,这个 1.5 也可以通过 options 传进来。

效果是这样的:

两个异步 chunk 分别为:

a + b + common:

a + b:

也就是说只把之前的 a + common 和 b + common 合并了,因为 common 模块比较大,所以合并之后的收益是挺大的。

这样就完成了 chunk 拆分的优化。

有的同学说,我平时也不用自己写插件来拆分 chunk 呀,webpack 不是提供了 SplitChunksPlugin 的插件么,还变成内置的了,配置下 optimization.splitChunks 就行。

没错,webpack 默认提供了拆分 chunk 的插件。

那这个插件是怎么实现的呢?

没错,SplitChunkPlugin 的实现原理就是我们刚才说的这些,注册了 optimizeChunks 的 hook,在里面做了 chunk 拆分:

它可以根据配置来拆分 chunk,但是终究是有局限性的。

如果某种 chunk 拆分方式它不支持呢?

我们就可以写插件自己拆分了,会自己拆分 chunk 之后,还不是想怎么分就怎么分么!

我们写的这个 webpack 插件的全部代码如下:

class ChunkTestPlugin {constructor(options) {this.options = options || {};}apply(compiler) {const options = this.options;const minSizeReduce = options.minSizeReduce || 1.5;compiler.hooks.compilation.tap("ChunkTestPlugin", compilation => {compilation.hooks.optimizeChunks.tap("ChunkTestPlugin", chunks => {const chunkGraph = compilation.chunkGraph;let combinations = [];for (const a of chunks) {if (a.canBeInitial()) continue;for (const b of chunks) {if (b.canBeInitial()) continue;if (b === a) break;const aSize = chunkGraph.getChunkSize(b, {chunkOverhead: 0});const bSize = chunkGraph.getChunkSize(a, {chunkOverhead: 0});const abSize = chunkGraph.getIntegratedChunksSize(b, a, {chunkOverhead: 0});const improvement = (aSize + bSize) / abSize;combinations.push({a,b,improvement});}}combinations.sort((a, b) => {return b.improvement - a.improvement;});const pair = combinations[0];if (!pair) return;if (pair.improvement < minSizeReduce) return;chunkGraph.integrateChunks(pair.b, pair.a);compilation.chunks.delete(pair.a);return true;});});}
}module.exports = ChunkTestPlugin; 

总结

webpack 的处理单位是模块,它的编译流程分为 make、seal、emit:

  • make:对入口模块分析依赖,构建 ModuleGraph,对每个模块调用 loader 处理。
  • seal:合并 Module 为 Chunk,合并之后 ModuleGraph 会变为 ChunkGraph。
  • emit:对每个 Chunk 通过模版打印成代码后输出

这个编译流程中有很多 hook,通过 tappable 的 api 组织,可以控制回调的同步、异步、串行、并行执行。

我们今天写的 Chunk 拆分插件,就是一个 SyncBailHook,同步熔断的串行 hook 类型,也就是前面回调返回 false 会终止后面的回调执行。

首先在 compiler 的 thisCompilation 的 hook 里来注册 compilation 的 optimizeChunks 的 hook。

在 optimizeChunks 的 hook 里可以拿到所有的 chunk,调用 chunkGraph 的 api 可以进行合并。

我们排除掉了入口 chunk,然后把剩下的 chunk 根据大小进行合并,达到了优化 chunk 的目的。

webpack 内置了 SplitChunksPlugin,但是毕竟有局限性,当不满足需求的时候就可以自己写插件来划分 chunk 了。

自己来控制 Chunk 划分,想怎么分就怎么分!

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享


http://chatgpt.dhexx.cn/article/yX0qIKTP.shtml

相关文章

堆漏洞挖掘中的Chunk分类(allocated chunk、free chunk、top chunk、last remainder chunk)

此图是在上篇文章介绍arena时用到的&#xff0c;我们可以看到&#xff1a;堆块被分为不同的种类&#xff0c;下面我们将来介绍这些。每一类就是一个malloc_chunk结构体&#xff0c;因为这些chunk同属于一个堆块&#xff0c;所以在一块连续的内存中&#xff0c;只是通过区域中特…

GBase 8s 块(chunk)

块是用于数据库服务器数据存储数据文件。可以是文件系统或裸设备。单个块的最大大小是 4TB。数据库最多可支持块数为 32,766 个。 在 UNIX 或 LINUX 上&#xff0c;建议使用裸设备&#xff0c;在 Windows 上&#xff0c;建议使用 NTFS文件系统。 下图对数据库块进行了图解说明…

Spring Batch 中的 chunk

我们都知道 Spring Batch 有 2 种任务方式。 主要是在 Step 阶段&#xff0c;在 Step 阶段&#xff0c;我们可以执行一个 Tasklet&#xff0c;我们也可以按照 Chunk 来执行。 主要区别 如果使用 Tasklet 的话&#xff0c;我们可以一个 Step 对应一个 Tasklet&#xff0c;Spr…

Webpack 理解 Chunk

期望 希望读过本篇文章&#xff0c;你在看Webpack配置的时候&#xff0c;能在脑中形成Chunk的生成过程。 Chunk Chunk不同于entry、 output、module这样的概念&#xff0c;它们对应着Webpack配置对象中的一个字段&#xff0c;Chunk没有单独的配置字段&#xff0c;但是这个词…

Http chunk介绍

总结&#xff1a; HTTP 1.1时&#xff0c;Response要嘛通过Content-Length来指定要传输的内容大小&#xff0c;要嘛通过Transfer-Encoding: chunked来传输动态大小的内容&#xff0c;此时要求Response传输的内容要符合chunk encoding的规定。 从抓包的角度来说&#xff0c;两个…

lua中chunk的理解

在Lua中&#xff0c;一个chunk是一段可执行的Lua代码。通常&#xff0c;一个chunk由一系列语句和表达式组成&#xff0c;可以是一个完整的程序文件&#xff0c;也可以是一个交互式控制台中输入的一行代码。 例如&#xff0c;下面是一个简单的Lua chunk&#xff1a; print(&qu…

【深度学习】Pytorch chunk函数

Pytorch chunk的方法做的是对张量进行分块&#xff0c;返回一个张量列表。但如果指定轴的元素个数被chunks除不尽&#xff0c;最后一块的元素个数会少。 torch.chunk(tensor, chunks, dim0) ->得到一个list的tensors 这个函数的作用是把一个tensor划分到特定数目的块。 chu…

不会还有人不知道module、bundle和chunk的区别吧?

module 非连续的功能块——提供了更小的表面积而不是整个程序。书写良好的modules提供了可靠的抽象及封装边界&#xff0c;组成了统一的设计和清晰的目的&#xff1b;模块解析&#xff08;ModuleResolution&#xff09;一个模块可以作为另一个模块的依赖模块&#xff0c;resolv…

torch.chunk

torch.chunk(tensor, chunk_num, dim)与torch.cat()原理相反&#xff0c;它是将tensor按dim&#xff08;行或列&#xff09;分割成chunk_num个tensor块&#xff0c;返回的是一个元组。 a torch.Tensor([[4,5,7], [3,9,8], [9,6,7]]) b torch.chunk(a, 3, dim 1) print(a) pri…

webpack中的chunk

Webpack 理解 Chunk - 掘金 Webpack 理解 Chunk 期望 希望读过本篇文章&#xff0c;你在看Webpack配置的时候&#xff0c;能在脑中形成Chunk的生成过程。 Chunk Chunk不同于entry、 output、module这样的概念&#xff0c;它们对应着Webpack配置对象中的一个字段&#xff0…

什么是chunk?

本文借鉴《自己动手实现Lua&#xff1a;虚拟机、编译器和标准库》&#xff0c;算是对自己学习的总结&#xff0c;也希望分享下所学知识~~ 什么叫Chunk&#xff1f; 一段可以被Lua解释器解释执行的代码就叫做chunk 可以很小&#xff0c;小到只有一两条语句&#xff1b;可以很大…

生日悖论matlab模拟

概率论课堂小作业 要求用matab模拟生日悖论 条件&#xff1a;30人||100次 本来想白嫖网上的解答 结果竟然找不到用matlab模拟仿真的 所幸不难 自己动手&#xff0c;也为后人铺路。 话不多说&#xff0c;直接上代码 clc clear m100; %仿真次数 N30;%学生人数 for j 1:mB zero…

Python关于生日悖论分析

生日悖论指如果一个房间里有23人或以上&#xff0c;那么至少有两个人生日相同的概率大于50%。编写程序&#xff0c;输出在10000例随机样本数量下&#xff0c;n个人中至少两个人生日相同的概率。&#xff08;n从10到50&#xff0c;不考虑闰年&#xff09; 代码&#xff1a; imp…

男孩女孩问题 生日悖论 三门问题

上篇文章 洗牌算法详解 讲到了验证概率算法的蒙特卡罗方法&#xff0c;今天聊点轻松的内容&#xff1a;几个和概率相关的有趣问题。 计算概率有下面两个最简单的原则&#xff1a; 原则一、计算概率一定要有一个参照系&#xff0c;称作「样本空间」&#xff0c;即随机事件可能…

生日悖论问题

生日悖论是指&#xff0c;如果一个房间裡有23个或23个以上的人&#xff0c;那么至少有两个人的生日相同的概率要大于50%。这就意味着在一个典型的标准小学班级(30人)中&#xff0c;存在两人生日相同的可能性更高。对于60或者更多的人&#xff0c;这种概率要大于99%。从引起逻辑…

生日悖论MATLAB仿真

生日悖论MATLAB仿真 终于熬过了这学期&#xff01;不知不觉大学的3/8已经过去了。回顾以下本学期让我印象最深刻&#xff0c;也最有成就感的事情是写出了我人生中第一个MATLAB程序&#xff01;由于先前零基础&#xff0c;所以从idea到code实现的整个过程是非常坎坷的QAQ那么话…

生日悖论 Birthday Paradox 至少有两人同一天生日概率

首先我们来看下生日悖论&#xff1a; 假设有n个人&#xff0c;365天的时间&#xff0c;假设所有人生日不相同的概率为&#xff08;1-P&#xff09; 第一个人可选择365 天中的任意365天&#xff0c;人数为1时所有人生日不相同的概率为365/365&#xff1b; 第二个人可选择365天…

【密码学/密码分析】生日悖论及生日攻击

生日悖论及生日攻击 鸽巢原理&#xff1a;给定n个鸽巢&#xff0c;至少存在n1只鸽子&#xff0c;那总是会发生碰撞。 概率环境&#xff1a;我们需要多少个物体&#xff08;鸽子&#xff09;使得发生碰撞的概率大于1/2&#xff1f; 答案是n1/2&#xff0c;而不是n/2。 举个例…

生日悖论的泛化问题的讨论

著名的生日悖论&#xff0c;不多言。 见维基百科&#xff1a; http://zh.wikipedia.org/wiki/%E7%94%9F%E6%97%A5%E6%82%96%E8%AE%BA 见百度百科&#xff1a; http://baike.baidu.com/view/859474.htm 摆渡、喂鸡&#xff0c;排名不分先后。 维基里面提到了泛化推广。生日…

关于生日悖论问题的验证

昨天在网上看到一个非常有意思的问题&#xff1a; 数学老师和体育老师打赌&#xff0c;数据老师认为在他们有50个人的班级里有两个生日是同一天的同学的概率远超没有的概率&#xff0c;反之是体育老师的观点。 第一次看到的时候我觉得这特数学老师才是教体育的吧&#xff0c; 我…