Technology Sharing

Webpack: Packaging logic of three Chunk products

2024-07-08

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Overview

  • In the previous article Webpack: Dependency Graph manages dependencies between modules In the previous chapter, we have explained in detail how to recursively read and parse module contents from Entry in the "Build" phase, and finally build the module dependency graph - ModuleGraph object. In this article, we will continue to explain how to organize Chunks according to the ModuleGraph content in the next "Encapsulation" phase, and further build the main process of ChunkGroup and ChunkGraph dependency objects.

In addition to the main process, we will also explain several vague concepts in detail:

  • What are Chunk, ChunkGroup, and ChunGraph objects? How do they interact with each other?
  • Webpack's default sub-packaging rules and problems in the rules.

ChunkGraph Construction Process

in front Init、Make、SealIn the previous section, we have introduced that Webpack's underlying construction logic can be roughly divided into:Initialization, construction, packaging"three phases:

insert image description here

in,"ConstructThe "phase is responsible for analyzing the dependencies between modules and establishing Dependency graph(ModuleGraph); Next, in "Encapsulation"In this stage, according to the dependency graph, the modules are encapsulated separately into several Chunk objects, and the parent-child dependency relationships between Chunks are sorted into ChunkGraph and several ChunkGroup objects.

The most important goal of the "encapsulation" phase is to build a ChunkGraph relationship graph based on the ModuleGraph relationship graph collected in the "construction" phase. The logic of this process is relatively complex:

Please add a description of the image

Let's briefly analyze the implementation logic of several important steps here.

The first step is critical: transferseal() After the function, traverseentry Configuration, creating an emptyChunk andEntryPoint Object (a specialChunkGroup), and initially set up the basic ChunkGraph Structural relationship, ready for the next step, key code:

class Compilation {
  seal(callback) {
    // ...
    const chunkGraphInit = new Map();
    // 遍历入口模块列表
    for (const [name, { dependencies, includeDependencies, options }] of this
      .entries) {
      // 为每一个 entry 创建对应的 Chunk 对象
      const chunk = this.addChunk(name);
      // 为每一个 entry 创建对应的 ChunkGroup 对象
      const entrypoint = new Entrypoint(options);
      // 关联 Chunk 与 ChunkGroup
      connectChunkGroupAndChunk(entrypoint, chunk);

      // 遍历 entry Dependency 列表
      for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {
        // 为每一个 EntryPoint 关联入口依赖对象,以便下一步从入口依赖开始遍历其它模块
        entrypoint.addOrigin(null, { name }, /** @type {any} */ (dep).request);

        const module = this.moduleGraph.getModule(dep);
        if (module) {
          // 在 ChunkGraph 中记录入口模块与 Chunk 关系
          chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
          // ...
        }
      }
    }
    // 调用 buildChunkGraph 方法,开始构建 ChunkGraph
    buildChunkGraph(this, chunkGraphInit);
    // 触发各种优化钩子
    // ...
  }
}

After execution is completed, the following data structure is formed:
insert image description here

Secondly, if you configure entry.runtimeWebpack also adds runtime code at this stage create The corresponding Chunk and directlydistribute Giveentry correspondingChunkGroupObject. Called when everything is ready buildChunkGraph function and proceed to the next step.

Step 2: existbuildChunkGraph Inside a functiontransfer visitModules Function, traverse ModuleGraph, assign all Modules to differentChunk object; if you encounterAsynchronous Module, then this module createnew ChunkGroup andChunk Object, and finally form the following data structure:
Please add a description of the image

third step: existbuildChunkGraph In the functiontransfer connectChunkGroups Method, establishChunkGroup between,Chunk The dependencies between them generate a completeChunkGraph Object, and finally form the following data structure:
Please add a description of the image

the fourth step: existbuildChunkGraph In the functiontransfer cleanupUnconnectedGroups Method, cleanup is invalidChunkGroup, which mainly plays a role in performance optimization.

After these four steps from top to bottom,ModuleGraph The modules stored in the will be assigned to three different Chunk objects: Entry, Async, and Runtime according to the nature of the modules themselves, and the dependencies between Chunks are stored in the ChunkGraph and ChunkGroup collections. The subpackaging strategy can be modified based on these objects later (for exampleSplitChunksPlugin), by reorganizing and allocating the ownership of Module and Chunk objects to achieve subpackaging optimization.

Chunk vs ChunkGroup vs ChunkGraph

The above construction process involves three key objects: Chunk, ChunkGroup, and ChunkGraph. Let's first summarize their concepts and functions to deepen our understanding:

  • Chunk:Module is used to read module content, record dependencies between modules, etc.; while Chunk merges multiple modules according to module dependencies and outputs them into asset files (the logic of merging and outputting products will be explained in the next chapter):

Please add a description of the image

  • ChunkGroup:one ChunkGroup Contains one or moreChunk Object;ChunkGroup andChunkGroup A parent-child dependency relationship is formed between them:

Please add a description of the image

  • ChunkGraph: Finally, Webpack will store the dependencies between Chunks and ChunkGroups in compilation.chunkGraph In the object, the following type of relationship is formed:
    Please add a description of the image

Default subcontracting rules

The above ChunkGraph The build process will eventually organize the Module into three different types of Chunks:

  • Entry Chunk: Same entry The modules reached below are organized into a Chunk;
  • Async Chunk: Asynchronous modules are organized into a single Chunk.
  • Runtime Chunk:entry.runtime When it is not empty, the runtime module will be organized into a separate Chunk.

This is built-in to Webpack. splitChunks Or other plug-ins, the default rules for mapping module input to output are one of the key underlying principles of Webpack, so it is necessary to introduce the specific rules of each Chunk.

Entry Chunk:

Starting with Entry Chunk, Webpack will first create a new entry chunk for each entry. entry createChunk For example, for the following configuration:

module.exports = {
  entry: {
    main: "./src/main",
    home: "./src/home",
  }
};

Traversal entry Object properties and createchunk[main]chunk[home] Two objects, the two Chunks containmainhome Modules:
insert image description here

After initialization, Webpack will ModuleGraph The dependency data willentry All modules touched by the next step are stuffed into the Chunk (occurs invisitModules Method), for example, for the following file dependencies:
insert image description here

main.js The four files a/b/c/d are directly or indirectly referenced in a synchronous manner. Webpack will firstmain.js The module creates Chunk and EntryPoint objects, and then gradually adds modules a/b/c/d tochunk[main] In the end, it forms:
insert image description here

Async Chunk:

Second, Webpack will convert each asynchronous import statement (import(xxx) andrequire.ensure) is processed into a separate Chunk object, and all its submodules are added to this Chunk - we call it Async Chunk. For example, for the following example:

// index.js
import './sync-a.js'
import './sync-b.js'

import('./async-a.js')

// async-a.js
import './sync-c.js'

In the entry module index.js In the sync-a and sync-b modules, import them synchronously; import the async-a module asynchronously; and import them synchronously in async-a.sync-c Modules form the following module dependency diagram:
Please add a description of the image

At this point, Webpack will be the entry index.js, asynchronous module async-a.js Create sub-packages separately to form the following Chunk structure:
insert image description here

and chunk[index] andchunk[async-a] A one-way dependency is formed between them, and Webpack will save this dependency inChunkGroup._parentsChunkGroup._children Properties.

Runtime Chunk:

Finally, except entryIn addition to asynchronous modules, Webpack5 also supports extracting runtime code into chunks. The runtime code here refers to a series of basic framework codes injected by Webpack to ensure that the packaged product can run normally. For example, the common Webpack packaged product structure is as follows:
Please add a description of the image
The large section of code circled in red in the figure above is the runtime code dynamically generated by Webpack. When compiling, Webpack will decide which runtime code to output based on the business code (based onDependency subclasses), for example:

  • need __webpack_require__.f__webpack_require__.r The functions such as the above provide the minimum modular support;
  • If you use the dynamic loading feature, you need to write __webpack_require__.e function;
  • If you use the Module Federation feature, you need to write __webpack_require__.o function;
  • etc.

Although each runtime code may be small, as features are added, the final result will become larger and larger. Especially for multi-entry applications, it is a bit wasteful to repeatedly package a similar runtime at each entry. For this reason, Webpack 5 provides entry.runtime Configuration items are used to declare how to package runtime code.entry Add a string to the itemruntime Values, for example:

module.exports = {
  entry: {
    index: { import: "./src/index", runtime: "solid-runtime" },
  }
};

exist compilation.seal function, Webpack firstentry createEntryPoint, then judge entry Is there a configurationruntime Attributes, if any, are createdruntime The value is the named Chunk, so the above configuration will generate two Chunks:chunk[index.js]chunk[solid-runtime], and finally produce two files:

  • Entry index corresponding to index.js document;
  • Runtime configuration corresponding to solid-runtime.js document.

In more entry In the scene, as long as eachentry All set the sameruntime value, the Webpack runtime code will be merged and written into the same Runtime Chunk, ultimately achieving product performance optimization. For example, for the following configuration:

module.exports = {
  entry: {
    index: { import: "./src/index", runtime: "solid-runtime" },
    home: { import: "./src/home", runtime: "solid-runtime" },
  }
};

Entrance indexhome Share the sameruntime Value, and finally generate three Chunks, namely:
Please add a description of the image

Entrance at this time chunk[index]chunk[home] With runtimechunk[solid-runtime] A parent-child dependency relationship will also be formed.

The problem of subcontracting rules

The biggest problem with the default subpackaging rule is that it cannot solve module duplication. If multiple chunks contain the same module at the same time, the module will be repeatedly packaged into these chunks without restriction. For example, suppose we have two entry points main/index that depend on the same module at the same time:
insert image description here

By default, Webpack will not do any additional processing for this, but simply pack the c module into two chunks, main/index, at the same time, and finally form:

insert image description here

can be seen chunk The modules are isolated from each other, and module c is packaged repeatedly, which may cause unnecessary performance loss to the final product!

To solve this problem, Webpack 3 introduced CommonChunkPlugin The plugin attempts to extract common dependencies between entries into separatechunk,but CommonChunkPlugin Essentially, it is still based on a simple parent-child relationship chain between Chunks. It is difficult to infer that the third package extracted should be used asentry Fatherchunk Still a childchunkCommonChunkPlugin Unified treatment for the parentchunk, which in some cases has a significant negative impact on performance.

For this reason, a more complex data structure was introduced after Webpack4. ChunkGroup Specialized in relationship chain management,SplitChunksPlugin Able to achieve more efficient and intelligentHeuristic subpackaging.

Summarize

In summary, the "Build" phase is responsible for building a ModuleGraph based on the reference relationship of the modules; the "Encapsulation" phase is responsible for building a series of Chunk objects based on the ModuleGraph, and organizing the dependencies between Chunks (asynchronous references, Runtime) into a ChunkGraph - a Chunk dependency graph object. Similar to the ModuleGraph, the introduction of the ChunkGraph structure can also decouple the management logic of the dependencies between Chunks, making the overall architecture logic more reasonable and easier to expand.

However, although it looks complicated, the most important goal of the "packaging" stage is to determine how many chunks there are and which modules are contained in each chunk - these are the key factors that truly affect the final packaging results.

To this end, we need to understand the three built-in subpackaging rules of Webpack5: Entry Chunk, Async Chunk and Runtime Chunk. These are the most primitive subpackaging logics. Other plugins (such as splitChunksPlugin) are all based on this, with the help of buildChunkGraph The various hooks triggered later further split, merge, and optimize the Chunk structure to achieve the extended subpackaging effect.

think Chunk Will there be only one product file produced? Why?mini-css-extract-pluginfile-loader How is this type of component that can write additional files implemented at the underlying level?