Author: Xiao Lei

Making: CommanderXL

This article explores how Webpack generates the corresponding Chunk graph from the Module graph created during compilation through source code.

Let’s start with some concepts and how they relate to each other:

  1. ChunkGroup, consisting of chunks. A chunkGroup can contain multiple chunks, which is used in generating/optimizing the Chunk graph.
  2. Chunk is composed of modules. A chunk can contain multiple modules. It is the final file output by Webpack after compilation and packaging.
  3. Module is a different resource file, which contains files provided in your code, such as JS/CSS/images, etc. During compilation, Webpack will combine and generate chunk according to the dependency relationship between different modules

We all know that webPack builds will depend on your specific business code and WebPack-related configuration to determine the final output file, the specific file name and file number will also depend on this. And those files are called chunks. For example, use asynchronous subcontracting apis in your business:

import('./foo.js').then(bar= > bar())
Copy the code

In the final output, foo.js will be exported as a separate chunk file.

Or, in your Webpack configuration, you’ve configured optimization to optimize chunk generation:

module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'runtime-chunk'}}}Copy the code

Finally, Webpack separates Webpack Runtime chunk into a chunk and outputs a file named Runtime-chunk. js.

These chunk files are composed of related module modules.

Let’s take a look at how WebPack generates chunk in a workflow. First, let’s take a look at an example:

// a.js (webpack config entry file)
import add from './b.js'

add(1.2)

import('./c').then(del= > del(1.2) -- -- -- -- --// b.js
import mod from './d.js'

export default function add(n1, n2) {
  return n1 + n2
}

mod(100.11) -- -- -- -- --// c.js
import mod from './d.js'

mod(100.11)

import('./b.js').then(add= > add(1.2))

export default function del(n1, n2) {
  return n1 - n2
}

-----

// d.js
export default function mod(n1, n2) {
  return n1 % n2
}
Copy the code

Webpack related configurations:


// webpack.config.js
module.exports = {
  entry: {
    app: 'a.js'
  },
  output: {
    filename: '[name].[chunkhash].js'.chunkFilename: '[name].bundle.[chunkhash:8].js'.publicPath: '/'
  },
  optimization: {
    runtimeChunk: {
      name: 'bundle'}}},Copy the code

Where, A. js is the entry file configured in webPack Config. A. js depends on B. js/ C. js, while B. js depends on D. js and C. js depends on D. js/ B. js. Finally compiled by Webpack, 3 chunk files will be generated, among which:

  • Bundle.js – contains the webPack Runtime Module code
  • App.bundle. js – contains the code for A.js /b.js/d.js
  • 2. Bundle. js – contains the code for C. js

Next, let’s look at the source code to see what kind of strategy webpack uses to complete the generation of chunk.

In the Webpack workflow, after all modules have been compiled, the seal phase starts to generate chunk related work:

// compilation.js

class Compilation {... seal () { ... this.hooks.beforeChunks.call();// Collect the _preparedEntrypoints array from the entry files in the addEntry method
		for (const preparedEntrypoint of this._preparedEntrypoints) {
			const module = preparedEntrypoint.module;
			const name = preparedEntrypoint.name;
			const chunk = this.addChunk(name); // The entry chunk is runtimeChunk
			const entrypoint = new Entrypoint(name); // Each entryPoint is a chunkGroup
			entrypoint.setRuntimeChunk(chunk); // Set Runtime chunk
			entrypoint.addOrigin(null, name, preparedEntrypoint.request);
			this.namedChunkGroups.set(name, entrypoint); // Set the contents of chunkGroups
			this.entrypoints.set(name, entrypoint);
			this.chunkGroups.push(entrypoint);

			// Set up the relationship between chunkGroup and chunk
			GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
			// Establish the relationship between chunk and module
			GraphHelpers.connectChunkAndModule(chunk, module);

			chunk.entryModule = module;
			chunk.name = name;

			this.assignDepth(module);
		}
		this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
		// Sort the modules
		this.sortModules(this.modules);
		// Create a hook after chunk
		this.hooks.afterChunks.call(this.chunks);
		//
		this.hooks.optimize.call();

		while (
			this.hooks.optimizeModulesBasic.call(this.modules) ||
			this.hooks.optimizeModules.call(this.modules) ||
			this.hooks.optimizeModulesAdvanced.call(this.modules)
		) {
			/* empty */
		}
		// Optimize the hook after module
		this.hooks.afterOptimizeModules.call(this.modules);
		while (
			this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
			this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
			// This is mainly about optimization configuration in Webpack Config
			this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
		) {
			/* empty */
		}
		// Optimize the hook after chunk
		this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups); . }... }Copy the code

In this process, the entry module configured in Webpack Config is first traversed. Each entry module will create a chunk through the addChunk method, and the new chunk is an empty chunk. That is, it contains no modules associated with it. Then instantiate an entryPoint, which is a chunkGroup. Each chunkGroup can contain multiple chunks. There is also a special internal runtimeChunk(the webPack Runtime code that is included when webPack is finally compiled is eventually injected into runtimeChunk). So far we have just created chunk and chunkGroup respectively, Then call GraphHelpers module provides connectChunkGroupAndChunk and connectChunkAndModule method to establish the connection between the chunkGroup and chunk, And the connection between chunk and the entry module (dependency modules are not covered here) :

// GraphHelpers.js

/** * @param {ChunkGroup} chunkGroup the ChunkGroup to connect * @param {Chunk} chunk chunk to tie to ChunkGroup * @returns {void} */
GraphHelpers.connectChunkGroupAndChunk = (chunkGroup, chunk) = > {
	if(chunkGroup.pushChunk(chunk)) { chunk.addGroup(chunkGroup); }};/** * @param {Chunk} chunk Chunk to connect to Module * @param {Module} module Module to connect to Chunk * @returns {void} */
GraphHelpers.connectChunkAndModule = (chunk, module) = > {
	if (module.addChunk(chunk)) {
		chunk.addModule(module); }};Copy the code

For example, in the example where only one entry module is configured, processing the entryPoints phase generates a chunkGroup and a chunk that currently contains only the entry Module. We all know that chunk output by Webpack contains related modules. In the compilation process, the connection between chunk and the entry module is only established at the above step, so how does chunk also establish the connection with other modules? Let’s take a look at how WebPack associates its dependent modules during chunk generation.

Related to this is compilation processDependenciesBlocksForChunkGroups method provided by the instance. The internal details of this method are more complex, and it contains two core processes:

  1. Basic Chunk Graph dependency graph is established by traversing module graph dependency graph.
  2. Go through the chunk graph dependency graph created in the first step and optimize the Chunk graph according to the previous Module graph. (Since the chunk graph is the basis for webpack to export chunk, In the process of this step, some chunks that have been created repeatedly in the chunk graph will be removed.

Let’s take a look at the internal process of this method through an overall flow chart:

Build chunk graph based on Module Graph

In the first step, the modules collected by compliation are traversed. In the process of traversing modules, the dependencies of this module are dealt with and the dependencies of this module are obtained. Module blocks are also handled, and each asynchronous block is added to the loop and treated as a module. Therefore, at the end of this traversal process, a basic Module graph will be built, which contains common modules and asynchronous modules (blocks), and finally stored in a map table (blockInfoMap) :

const iteratorBlockPrepare = b= > {
  blockInfoBlocks.push(b);
  // Add blocks to the blockQueue to proceed to the next iteration
  blockQueue.push(b);
};

// This compilation contains all modules
for (const modules of this.modules) {
  blockQueue = [module];
  currentModule = module;
  while (blockQueue.length > 0) {
    block = blockQueue.pop(); // The module currently being traversed
    blockInfoModules = new Set(a);// module dependent synchronized module
    blockInfoBlocks = []; // Module dependent asynchronous module(block)

    if (block.variables) {
      iterationBlockVariable(block.variables, iteratorDependency);
    }

    // Add the generic module of Dependencies to the blockInfoModules dataset (SET)
    if (block.dependencies) {
      iterationOfArrayCallback(block.dependencies, iteratorDependency);
    }

    // Add asynchronous modules (blocks) to the blockInfoBlocks and blockQueue arrays so that they are added to the blockQueue
    // Module also goes through the loop to get asynchronous Module (block) dependencies
    if (block.blocks) {
      iterationOfArrayCallback(block.blocks, iteratorBlockPrepare);
    }

    const blockInfo = {
      modules: Array.from(blockInfoModules),
      blocks: blockInfoBlocks
    };
    // blockInfoMap stores synchronous modules and asynchronous blocks dependent on each moduleblockInfoMap.set(block, blockInfo); }}Copy the code

The generated Module graph in our example is:

Once the basic Module Graph (blockInfoMap) is generated, the next step is to generate the Basic Chunk Graph from the Module Graph. The entryPoint(chunkGroup) is converted to a new queue. Each entry in the queue array contains:

  • Action (Type of module to be processed. Modules of different processing types go through different processes, starting with ENTER_MODULE(1))
  • Block (entry Module)
  • Module (Entry Module)
  • Chunk (empty chunk created for each entry module at the beginning of the SEAL phase)
  • ChunkGroup (entryPoint is the type of chunkGroup)

In the example we provide, there is only one entry after the queue is initialized because it is single-entry.

{
  action: ENTER_MODULE,
  block: a.js,
  module: a.js,
  chunk,
  chunkGroup: entryPoint
}
Copy the code

Now we’re going to go through the queue

// Create an asynchronous block
// For each async Block in graph
/** * @param {AsyncDependenciesBlock} b iterating over each Async DepBlock * @returns {void} */
const iteratorBlock = b= > {
  // 1. We create a chunk for this Block
  // but only once (blockChunkGroups map)
  let c = blockChunkGroups.get(b);
  if (c === undefined) {
    c = this.namedChunkGroups.get(b.chunkName);
    if (c && c.isInitial()) {
      this.errors.push(
        new AsyncDependencyToInitialChunkError(b.chunkName, module, b.loc)
      );
      c = chunkGroup;
    } else {
      // Create a new chunkGroup and chunk using the addChunkInGroup method and return the chunkGroup
      c = this.addChunkInGroup(
        b.groupOptions || b.chunkName,
        module.// The module to which this block belongs
        b.loc,
        b.request
      );
      chunkGroupCounters.set(c, { index: 0.index2: 0}); blockChunkGroups.set(b, c); allCreatedChunkGroups.add(c); }}else {
    // TODO webpack 5 remove addOptions check
    if (c.addOptions) c.addOptions(b.groupOptions);
    c.addOrigin(module, b.loc, b.request);
  }

  // 2. We store the Block+Chunk mapping as dependency for the chunk
  let deps = chunkDependencies.get(chunkGroup);
  if(! deps) chunkDependencies.set(chunkGroup, (deps = []));// The block and chunkGroup on which the current chunkGroup depends
  deps.push({
    block: b,
    chunkGroup: c,
    couldBeFiltered: true
  });
  // Asynchronous blocks use the new chunkGroup created
  // 3. We enqueue the DependenciesBlock for traversal
  queueDelayed.push({
    action: PROCESS_BLOCK,
    block: b,
    module: module.chunk: c.chunks[0].// Get the first chunk of the newly created chunkGroup, that is, the chunk to which the block needs to be added
    chunkGroup: c // Asynchronous blocks use the newly created chunkGroup}); }; . const ADD_AND_ENTER_MODULE =0;
const ENTER_MODULE = 1;
const PROCESS_BLOCK = 2;
const LEAVE_MODULE = 3; . const chunkGroupToQueueItem =chunkGroup= > ({
  action: ENTER_MODULE,
  block: chunkGroup.chunks[0].entryModule,
  module: chunkGroup.chunks[0].entryModule,
  chunk: chunkGroup.chunks[0],
  chunkGroup
});

let queue = inputChunkGroups.map(chunkGroupToQueueItem).reverse()

while (queue.length) { // Queue traversal
  while (queue.length) { // Inner queue traversal
    const queueItem = queue.pop();
    module = queueItem.module;
    block = queueItem.block;
    chunk = queueItem.chunk;
    chunkGroup = queueItem.chunkGroup;

    switch (queueItem.action) {
      case ADD_AND_ENTER_MODULE: {
        // Add module to chunk
        // We connect Module and Chunk when not already done
        if (chunk.addModule(module)) {
          module.addChunk(chunk);
        } else {
          // already connected, skip it
          break; }}// fallthrough
      case ENTER_MODULE: {
        ...
        queue.push({
          action: LEAVE_MODULE,
          block,
          module,
          chunk,
          chunkGroup
        });
      }
      // fallthrough
      case PROCESS_BLOCK: {
        // get prepared block info
        const blockInfo = blockInfoMap.get(block);
        // Traverse all referenced modules
        for (let i = blockInfo.modules.length - 1; i >= 0; i--) {
          const refModule = blockInfo.modules[i];
          if (chunk.containsModule(refModule)) {
            // skip early if already connected
            continue;
          }
          // enqueue the add and enter to enter in the correct order
          // this is relevant with circular dependencies
          queue.push({
            action: ADD_AND_ENTER_MODULE,
            block: refModule, / / rely on the module
            module: refModule, / / rely on the module
            chunk, // Chunk to which module belongs
            chunkGroup // chunkGroup to which module belongs
          });
        }

        // Start creating asynchronous chunks
        // Traverse all Blocks
        iterationOfArrayCallback(blockInfo.blocks, iteratorBlock);

        if (blockInfo.blocks.length > 0 && module! == block) { blocksWithNestedBlocks.add(block); }break;
      }
      caseLEAVE_MODULE: { ... break; }}}const tempQueue = queue;
  queue = queueDelayed.reverse();
  queueDelayed = tempQueue;
}
Copy the code

The queue is traversed twice (inner layer and outer layer). We will explain why it needs to be traversed twice later. First, let’s look at the inner traversal operation. First, enter the corresponding processing flow according to the type of action:

When the ENTRY_MODULE phase is complete, an entry with action LEAVE_MODULE will be added to the queue and used later in the process. When the ENTRY_MODULE phase is complete, The PROCESS_BLOCK stage is immediately entered:

In this stage, blockInfoMap gets synchronous dependency modules and asynchronous dependency blocks for this module (called A) based on the module graph dependency graph.

Modules’ chunks (called B) are then iterated to see if the chunk that module(A) currently belongs to contains its dependent module(B). If not, new entries will be added to the queue. The action of the newly added item is ADD_AND_ENTER_MODULE, that is, the new item will enter the ADD_AND_ENTER_MODULE phase first when traversed next time.

When the new item is pushed to the queue, that is, the module dependent module(A) that has not yet been processed is added to the queue, We then call the iteratorBlock method to handle all asynchronous blocks that this module(A) depends on. Inside this method we do the following:

  1. Call addChunkInGroup to create a chunk and a chunkGroup for the asynchronous block, At the same time call GraphHelpers module provides connectChunkGroupAndChunk set up the relationship between the new chunk and chunkGroup. When you load a module using the asynchronous API in your code, WebPack will eventually output a chunk to the module, but this chunk is an empty chunk without adding any dependent modules.

  2. Create dependencies between chunkGroup, block, and chunkGroup of the current module and store them in the chunkDependencies Map. This Map table is mainly used to optimize the Chunk graph later;

  3. Add an action of type PROCESS_BLOCK to queueDelayed, module to the module to which the current module belongs, and block to the asynchronous module that the current Module depends on, While chunk(the first chunk of a chunkGroup) and chunkGroup both handle new items generated by the asynchronous module, the new items added to the queueDelayed dataset here are mainly for the outer layer traversal of the queue.

In the ENTRY_MODULE phase, the dependency modules of entry Module are added to the queue. After this phase, the queue enters the second round of traversal:

When traversing the inner layer of the queue, we focus on each item in the queue whose action type is ADD_AND_ENTER_MODULE. During the actual processing, we enter the ADD_AND_ENTER_MODULE stage. The main work done in this phase is to determine whether the module on which chunk depends has been added to the chunk (chunk.addModule method). If not, the module is added to the chunk. Enter the ENTRY_MODULE phase, enter the subsequent process (see above), and skip the traversal if it has already been added.

When the inner traversal of the queue is complete (each inner traversal corresponds to the same chunkGroup, i.e. each inner traversal processes all modules contained in the chunkGroup), the outer traversal begins. The queueDelayed data set is processed.

Above is inside the processDependenciesBlocksForChunkGroups method for module graph and the chunk graph of preliminary treatment, The end result is a chunk graph based on the Module graph, adding module dependencies to empty chunks.

EntryPoint contains three modules A, B, and D. The asynchronous dependency module C of A and the synchronous dependency module D of C belong to the newly created chunkGroup2. There is only one chunk in chunkGroup2. C’s asynchronous module B belongs to the newly created chunkGroup3.

To optimize the chunk graph

Next, it enters the second step, traversing the chunk graph and establishing parent-child relationships between different chunkgroups by using relationships with dependent modules, while eliminating some chunks that have not established relationships.

/** * Helper function to check if all modules of a chunk are available * * @param {ChunkGroup} chunkGroup the chunkGroup  to scan * @param {Set
      
       } availableModules the comparitor set * @returns {boolean} return true if all modules of a  chunk are available */
      
// Check whether the chunkGroup contains all availableModules
const areModulesAvailable = (chunkGroup, availableModules) = > {
  for (const chunk of chunkGroup.chunks) {
    for (const module of chunk.modulesIterable) {
      // If there is no module in availableModules, return false
      if(! availableModules.has(module)) return false; }}return true;
};

// For each edge in the basic chunk graph
/** * @param {TODO} dep the dependency used for filtering * @returns {boolean} used to filter "edges" (aka Dependencies)  that were pointing * to modules that are already available. Also filters circular dependencies in the chunks graph */
const filterFn = dep= > {
  const depChunkGroup = dep.chunkGroup;
  if(! dep.couldBeFiltered)return true;
  if (blocksWithNestedBlocks.has(dep.block)) return true;
  if (areModulesAvailable(depChunkGroup, newAvailableModules)) {
    return false; // break, all modules are already available
  }
  dep.couldBeFiltered = false;
  return true;
};

/** @type {Map<ChunkGroup, ChunkGroupInfo>} */
const chunkGroupInfoMap = new Map(a);/** @type {Queue<ChunkGroup>} */
const queue2 = new Queue(inputChunkGroups);
for (const chunkGroup of inputChunkGroups) {
  chunkGroupInfoMap.set(chunkGroup, {
    minAvailableModules: undefined.availableModulesToBeMerged: [new Set(a)]}); }... while (queue2.length) { chunkGroup = queue2.dequeue();const info = chunkGroupInfoMap.get(chunkGroup);
  const availableModulesToBeMerged = info.availableModulesToBeMerged;
  letminAvailableModules = info.minAvailableModules; . }...Copy the code

The chunkGroupInfoMap contains different chunkGroup information:

  • MinAvailableModules (the smallest module data set that chunkGroup can track)
  • AvailableModulesToBeMerged (traversal link used by the module set)
/** @type {Map<ChunkGroup, ChunkGroupInfo>} */
const chunkGroupInfoMap = new Map(a);/** @type {Queue<ChunkGroup>} */
const queue2 = new Queue(inputChunkGroups);
for (const chunkGroup of inputChunkGroups) {
  chunkGroupInfoMap.set(chunkGroup, {
    minAvailableModules: undefined.availableModulesToBeMerged: [new Set()]
  });
}
Copy the code

Once complete, we iterate over Queue2, each of which is a chunkGroup, starting with the chunkGroup corresponding to the entry. In our example, the dynamically loaded module C is also added to queue2 as an “independent” entry. However, there is a parent-child relationship, which depends on the chunkGroup corresponding to the entry.

while (queue2.length) {
  chunkGroup = queue2.dequeue();
  const info = chunkGroupInfoMap.get(chunkGroup);
  const availableModulesToBeMerged = info.availableModulesToBeMerged;
  let minAvailableModules = info.minAvailableModules;

  // 1. Get minimal available modules
  // It doesn't make sense to traverse a chunk again with more available modules.
  // This step calculates the minimal available modules and skips traversal when
  // the list didn't shrink.
  availableModulesToBeMerged.sort(bySetSize);
  let changed = false;
  for (const availableModules of availableModulesToBeMerged) {
    if (minAvailableModules === undefined) {
      minAvailableModules = new Set(availableModules);
      info.minAvailableModules = minAvailableModules;
      changed = true;
    } else {
      for (const m of minAvailableModules) {
        if(! availableModules.has(m)) { minAvailableModules.delete(m); changed =true;
        }
      }
    }
  }
  availableModulesToBeMerged.length = 0;
  if(! changed)continue;

  // Get the deps array of this chunkGroup, containing asynchronous blocks and corresponding chunkgroups
  // 2. Get the edges at this point of the graph
  const deps = chunkDependencies.get(chunkGroup);
  if(! deps)continue;
  if (deps.length === 0) continue;

  // Create a new data set of newAvailableModules based on the previous minAvailableModules
  // That is, all modules in the chunk traversed before will be stored in this data set, continuously accumulating
  // 3. Create a new Set of available modules at this points
  newAvailableModules = new Set(minAvailableModules);
  for (const chunk of chunkGroup.chunks) {
    for (const m of chunk.modulesIterable) { // The module contained in this chunknewAvailableModules.add(m); }}// Boundary conditions, and the chunkGroup of asynchronous blocks
  // 4. Foreach remaining edge
  const nextChunkGroups = new Set(a);// Asynchronous block dependency
  for (let i = 0; i < deps.length; i++) {
    const dep = deps[i];

    // Filter inline, rather than creating a new array from `.filter()`
    if(! filterFn(dep)) {continue;
    }
    // The chunkGroup to which this block belongs is created inside the iteratorBlock method
    const depChunkGroup = dep.chunkGroup;
    const depBlock = dep.block;

    // Start to establish the relationship between block and chunkGroup
    // When a new chunk is created for a block, only the relationship between chunkGroup and chunk is established.
    // 5. Connect block with chunk
    GraphHelpers.connectDependenciesBlockAndChunkGroup(
      depBlock,
      depChunkGroup
    );

    // Establishes a connection between the newly created chunkGroup and the previous chunkGroup
    // 6. Connect chunk with parent
    GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup);

    nextChunkGroups.add(depChunkGroup);
  }

  // 7. Enqueue further traversal
  for (const nextChunkGroup of nextChunkGroups) {
    ...

    // As queue deduplicates enqueued items this makes sure that a ChunkGroup
    // is not enqueued twicequeue2.enqueue(nextChunkGroup); }}Copy the code

Gets the dePS array dependencies for chunkgroups cached in the chunkDependencies section of the first stage, which holds asynchronous blocks on which different chunkgroups depend. And the chunkGroup created with the block (currently they only exist in a map structure and there is no dependency between chunkGroup and block).

If the DEPS data does not exist or has a length of 0, the chunkGroup process in dePS will be skipped. Otherwise, a new available Module dataset, newAvailableModules, will be created for the chunkGroup. Start traversing all modules contained in chunks of the chunkGroup and add them to the data set newAvailableModules. And start iterating through the chunkGroup’s DEPS array dependencies.

  1. Determine the newAvailableModules provided by the chunkGroup (which can be understood as setA, the set of all modules of the chunkGroup) and the chunkgroups in the DEPS dependencies (chunkgroups created by asynchronous blocks) contains all sets of modules (setBs) in the chunk containing them:
  • If setB has modules (usually asynchronous blocks) that setA does not have, They are treated in the Chunk graph (edge condition), which means that the setA of the modules in the chunk that have been traversed does not contain all the modules in use. These unincluded modules exist in the chunkGroup of the DEps dependency, so you need to continue traversing the chunkGroup of the DEPS dependency
  • If all modules in setB already exist in setA, it means that all modules used in the dependent chunkGroup have already been included in the chunk that has been traversed so far, then there is no need to go through the following process and skip directly. Next dependency traversal;
  1. Helper functions provided through the GraphHelpers moduleconnectDependenciesBlockAndChunkGroupEstablish asynchronous block and chunkGroup dependency in DEPS dependency;
  2. Helper functions provided through the GraphHelpers moduleconnectChunkGroupParentAndChildEstablishes dependencies between chunkgroups and chunkgroups in dePS dependencies(This dependency also determines whether the files output after webPack compilation will contain chunks of the chunkGroup in the DEPS dependency.);
  3. Adding the chunkgroups from the DEPS dependency to the nextChunkGroups dataset is the next step to traverse the newly added Chunkgroups.
  4. When all of the above traversal is complete, the data set (allCreatedChunkGroups) created by processing asynchronous blocks is traversed, Start processing chunkgroups without dependencies (the dependencies between chunkgroups were established in step 3 at 👆). If you encounter chunkgroups without any dependencies, All chunks contained in these chunkgroups are removed from the Chunk graph dependency graph. Eventually these chunks are not generated when the output file is finished by the Webpack compilation process.

So in our example, after the steps mentioned above, the first stage handles the entryPoint(chunkGroup) and all its modules, and discovers that the entryPoint depends on the asynchronous block C. It is contained in the block with NestedBlocks data set and, according to the corresponding filtering rules, is required to continue traversing chunkGroup2 where asynchronous block C is located. It then relies on chunkGroup3 for processing chunkGroup2, which contains asynchronous block D, because a collection of modules is completed during the first stage of processing entryPoint, This includes synchronous module D, and it is possible to imagine that synchronous module D and asynchronous block D can only output one, and synchronous Module D has a higher priority than asynchronous block D. Therefore, the code of module D will be exported to entryPoint chunk as synchronous module D, so that the chunkGroup3 containing asynchronous block D will not be exported accordingly. It gets removed from the Chunk graph.

The final chunk dependency graph will be generated:

This is a source code analysis of how WebPack builds the Module graph and how the Module Graph is used to generate the Chunk graph. After you finish reading this article, you should have a general understanding of how webpack builds the Chunk graph. What is the process by which the different chunks appear in the target output folder in your project application.