AsyncChunkWaterfallsPlugin.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const AsyncChunkWaterfallWarning = require("../errors/AsyncChunkWaterfallWarning");
  7. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  8. /** @import ChunkGroup from "../ChunkGroup" */
  9. /** @import Compiler from "../Compiler" */
  10. /** @import { WaterfallDetails } from "../errors/AsyncChunkWaterfallWarning" */
  11. const PLUGIN_NAME = "AsyncChunkWaterfallsPlugin";
  12. // Enough to name the offenders without printing the whole chunk graph.
  13. const MAX_REPORTED_WATERFALLS = 5;
  14. // Two levels is the shape `import()` is for — a route that loads its own data.
  15. // Three is where the round trips start outweighing what splitting saved.
  16. const MIN_REPORTED_DEPTH = 3;
  17. class AsyncChunkWaterfallsPlugin {
  18. /**
  19. * Creates an instance of AsyncChunkWaterfallsPlugin.
  20. * @param {PerformanceOptions} options the plugin options
  21. */
  22. constructor(options) {
  23. /** @type {PerformanceOptions["hints"]} */
  24. this.hints = options.hints;
  25. }
  26. /**
  27. * Applies the plugin by registering its hooks on the compiler.
  28. * @param {Compiler} compiler the compiler instance
  29. * @returns {void}
  30. */
  31. apply(compiler) {
  32. const hints = this.hints;
  33. if (!hints) return;
  34. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  35. // `afterSeal` is past the hash, which folds every message into it — a
  36. // hint reported earlier would change the build's identity.
  37. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  38. const { chunkGraph } = compilation;
  39. /**
  40. * The name to print for a group; ids exist by `afterSeal`.
  41. * @param {ChunkGroup} group the group
  42. * @returns {string} what to call it
  43. */
  44. const nameOf = (group) => {
  45. if (group.name) return group.name;
  46. const [chunk] = group.chunks;
  47. return chunk && chunk.id !== null ? `${chunk.id}` : "(unnamed)";
  48. };
  49. // Breadth-first from the initial groups, so the depth recorded for a
  50. // group is the fewest requests a client makes to reach it.
  51. /** @type {Map<ChunkGroup, ChunkGroup[]>} */
  52. const pathTo = new Map();
  53. /** @type {ChunkGroup[]} */
  54. const queue = [];
  55. for (const group of compilation.chunkGroups) {
  56. if (!group.isInitial()) continue;
  57. pathTo.set(group, []);
  58. queue.push(group);
  59. }
  60. /** @type {WaterfallDetails[]} */
  61. const waterfalls = [];
  62. let deepest = 0;
  63. for (let i = 0; i < queue.length; i++) {
  64. const group = queue[i];
  65. const path = /** @type {ChunkGroup[]} */ (pathTo.get(group));
  66. for (const child of group.getChildren()) {
  67. if (pathTo.has(child)) continue;
  68. const childPath = [...path, child];
  69. pathTo.set(child, childPath);
  70. queue.push(child);
  71. if (childPath.length < MIN_REPORTED_DEPTH) continue;
  72. // Only the end of a chain is reported: every prefix of it is a
  73. // waterfall too, and naming them all says the same thing N times.
  74. if (child.getNumberOfChildren() > 0) continue;
  75. let size = 0;
  76. for (const step of childPath) {
  77. for (const chunk of step.chunks) {
  78. size += chunkGraph.getChunkSize(chunk);
  79. }
  80. }
  81. deepest = Math.max(deepest, childPath.length);
  82. waterfalls.push({ chain: childPath.map(nameOf), size });
  83. }
  84. }
  85. if (waterfalls.length === 0) return;
  86. // Deepest first, then largest: the worst round-trip cost leads.
  87. waterfalls.sort(
  88. (a, b) => b.chain.length - a.chain.length || b.size - a.size
  89. );
  90. const warning = new AsyncChunkWaterfallWarning(
  91. waterfalls.slice(0, MAX_REPORTED_WATERFALLS),
  92. deepest
  93. );
  94. if (hints === "error") {
  95. compilation.errors.push(warning);
  96. } else if (hints === "stats") {
  97. compilation.hints.push(warning);
  98. } else {
  99. compilation.warnings.push(warning);
  100. }
  101. });
  102. });
  103. }
  104. }
  105. module.exports = AsyncChunkWaterfallsPlugin;