SizeLimitsPlugin.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Sean Larkin @thelarkinn
  4. */
  5. "use strict";
  6. const RuntimeGlobals = require("../RuntimeGlobals");
  7. const RuntimeModule = require("../RuntimeModule");
  8. const { find } = require("../util/SetHelpers");
  9. const { compareStrings } = require("../util/comparators");
  10. const AssetsOverSizeLimitWarning = require("./AssetsOverSizeLimitWarning");
  11. const EntrypointsOverSizeLimitWarning = require("./EntrypointsOverSizeLimitWarning");
  12. const NoAsyncChunksWarning = require("./NoAsyncChunksWarning");
  13. const RuntimeInLargeChunkWarning = require("./RuntimeInLargeChunkWarning");
  14. const getModuleSize = require("./getModuleSize");
  15. /** @import { Source } from "webpack-sources" */
  16. /** @import { PerformanceOptions } from "../../declarations/WebpackOptions" */
  17. /** @import Chunk from "../Chunk" */
  18. /** @import ChunkGraph from "../ChunkGraph" */
  19. /** @import ChunkGroup from "../ChunkGroup" */
  20. /** @import Compilation, { Asset } from "../Compilation" */
  21. /** @import Compiler from "../Compiler" */
  22. /** @import Module from "../Module" */
  23. /** @import Entrypoint from "../Entrypoint" */
  24. /** @import WebpackError from "../errors/WebpackError" */
  25. /**
  26. * Defines the module details type used by this module.
  27. * @typedef {object} ModuleDetails
  28. * @property {string} name
  29. * @property {number} size
  30. */
  31. /**
  32. * Defines the asset details type used by this module.
  33. * @typedef {object} AssetDetails
  34. * @property {string} name
  35. * @property {number} size
  36. * @property {ModuleDetails[]=} modules
  37. */
  38. /**
  39. * Defines the entrypoint details type used by this module.
  40. * @typedef {object} EntrypointDetails
  41. * @property {string} name
  42. * @property {number} size
  43. * @property {string[]} files
  44. */
  45. /** @type {WeakSet<Entrypoint | ChunkGroup | Source>} */
  46. const isOverSizeLimitSet = new WeakSet();
  47. /** @typedef {(name: Asset["name"], source: Asset["source"], assetInfo: Asset["info"]) => boolean} AssetFilter */
  48. /** @type {AssetFilter} */
  49. const excludeSourceMap = (name, source, info) => !info.development;
  50. // Runtime globals whose generated code describes the other chunks, so the chunk
  51. // holding them is rewritten whenever anything else in the build changes.
  52. const BUILD_WIDE_RUNTIME_GLOBALS = [
  53. RuntimeGlobals.getChunkScriptFilename,
  54. RuntimeGlobals.getChunkCssFilename,
  55. RuntimeGlobals.getChunkUpdateScriptFilename,
  56. RuntimeGlobals.getFullHash,
  57. RuntimeGlobals.getUpdateManifestFilename
  58. ];
  59. /**
  60. * Tells whether an entrypoint ships its runtime inside a chunk that also carries
  61. * modules, and that runtime describes the rest of the build. Only then does
  62. * `optimization.runtimeChunk` win anything.
  63. * @param {ChunkGraph} chunkGraph the chunk graph
  64. * @param {Entrypoint} entrypoint an entrypoint
  65. * @returns {boolean} true when splitting the runtime off would keep the chunk stable
  66. */
  67. const hasEmbeddedRuntime = (chunkGraph, entrypoint) => {
  68. const runtimeChunk = entrypoint.getRuntimeChunk();
  69. if (!runtimeChunk) return false;
  70. // A chunk of its own carries the runtime modules and nothing else. Asking each
  71. // module beats counting, which depends on runtime modules being counted twice.
  72. let carriesCode = false;
  73. for (const module of chunkGraph.getChunkModulesIterable(runtimeChunk)) {
  74. if (!(module instanceof RuntimeModule)) {
  75. carriesCode = true;
  76. break;
  77. }
  78. }
  79. if (!carriesCode) return false;
  80. // The tree requirements are the ones the emitted runtime modules answer to.
  81. const runtimeRequirements =
  82. chunkGraph.getTreeRuntimeRequirements(runtimeChunk);
  83. // A global nothing here names stays silent, so a new one costs a hint, not a wrong one.
  84. return BUILD_WIDE_RUNTIME_GLOBALS.some((runtimeGlobal) =>
  85. runtimeRequirements.has(runtimeGlobal)
  86. );
  87. };
  88. // Enough to point at the culprit without turning the hint into a report.
  89. const MAX_REPORTED_MODULES = 3;
  90. /**
  91. * Names the largest modules inside each oversized asset. "This file is too big"
  92. * is only actionable once you know what fills it.
  93. * @param {Compilation} compilation the compilation
  94. * @param {AssetDetails[]} assetsOverSizeLimit the oversized assets, annotated in place
  95. * @returns {void}
  96. */
  97. const addLargestModules = (compilation, assetsOverSizeLimit) => {
  98. const { chunkGraph, requestShortener } = compilation;
  99. /** @type {Map<string, Chunk[]>} */
  100. const chunksByFile = new Map();
  101. for (const chunk of compilation.chunks) {
  102. // A chunk emitting several files (javascript plus extracted css, …) gives
  103. // no way to tell which module ended up in which, so it names none.
  104. if (chunk.files.size !== 1) continue;
  105. for (const file of chunk.files) {
  106. const chunks = chunksByFile.get(file);
  107. if (chunks === undefined) {
  108. chunksByFile.set(file, [chunk]);
  109. } else {
  110. chunks.push(chunk);
  111. }
  112. }
  113. }
  114. for (const asset of assetsOverSizeLimit) {
  115. const chunks = chunksByFile.get(asset.name);
  116. // An asset a loader or plugin emitted belongs to no chunk, so nothing
  117. // describes its contents; likewise one whose chunk emits several files.
  118. if (chunks === undefined) continue;
  119. /** @type {Set<Module>} */
  120. const seen = new Set();
  121. /** @type {ModuleDetails[]} */
  122. const modules = [];
  123. for (const chunk of chunks) {
  124. for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
  125. if (seen.has(module)) continue;
  126. seen.add(module);
  127. modules.push({
  128. name: module.readableIdentifier(requestShortener),
  129. size: getModuleSize(module)
  130. });
  131. }
  132. }
  133. if (modules.length === 0) continue;
  134. // Ties break by name: which modules finish first is not stable.
  135. modules.sort((a, b) => b.size - a.size || compareStrings(a.name, b.name));
  136. asset.modules = modules.slice(0, MAX_REPORTED_MODULES);
  137. }
  138. };
  139. const PLUGIN_NAME = "SizeLimitsPlugin";
  140. module.exports = class SizeLimitsPlugin {
  141. /**
  142. * Creates an instance of SizeLimitsPlugin.
  143. * @param {PerformanceOptions} options the plugin options
  144. */
  145. constructor(options) {
  146. /** @type {PerformanceOptions["hints"]} */
  147. this.hints = options.hints;
  148. /** @type {number | undefined} */
  149. this.maxAssetSize = options.maxAssetSize;
  150. /** @type {number | undefined} */
  151. this.maxEntrypointSize = options.maxEntrypointSize;
  152. /** @type {AssetFilter | undefined} */
  153. this.assetFilter = options.assetFilter;
  154. }
  155. /**
  156. * Checks whether this size limits plugin is over size limit.
  157. * @param {Entrypoint | ChunkGroup | Source} thing the resource to test
  158. * @returns {boolean} true if over the limit
  159. */
  160. static isOverSizeLimit(thing) {
  161. return isOverSizeLimitSet.has(thing);
  162. }
  163. /**
  164. * Applies the plugin by registering its hooks on the compiler.
  165. * @param {Compiler} compiler the compiler instance
  166. * @returns {void}
  167. */
  168. apply(compiler) {
  169. const entrypointSizeLimit = this.maxEntrypointSize;
  170. const assetSizeLimit = this.maxAssetSize;
  171. const hints = this.hints;
  172. const assetFilter = this.assetFilter || excludeSourceMap;
  173. compiler.hooks.afterEmit.tap(PLUGIN_NAME, (compilation) => {
  174. /** @type {WebpackError[]} */
  175. const warnings = [];
  176. /**
  177. * Gets entrypoint size.
  178. * @param {Entrypoint} entrypoint an entrypoint
  179. * @returns {number} the size of the entrypoint
  180. */
  181. const getEntrypointSize = (entrypoint) => {
  182. let size = 0;
  183. for (const file of entrypoint.getFiles()) {
  184. const asset = compilation.getAsset(file);
  185. if (
  186. asset &&
  187. assetFilter(asset.name, asset.source, asset.info) &&
  188. asset.source
  189. ) {
  190. size += asset.info.size || asset.source.size();
  191. }
  192. }
  193. return size;
  194. };
  195. /** @type {AssetDetails[]} */
  196. const assetsOverSizeLimit = [];
  197. for (const { name, source, info } of compilation.getAssets()) {
  198. if (!assetFilter(name, source, info) || !source) {
  199. continue;
  200. }
  201. const size = info.size || source.size();
  202. if (size > /** @type {number} */ (assetSizeLimit)) {
  203. assetsOverSizeLimit.push({
  204. name,
  205. size
  206. });
  207. isOverSizeLimitSet.add(source);
  208. }
  209. }
  210. /**
  211. * Returns result.
  212. * @param {Asset["name"]} name the name
  213. * @returns {boolean | undefined} result
  214. */
  215. const fileFilter = (name) => {
  216. const asset = compilation.getAsset(name);
  217. return asset && assetFilter(asset.name, asset.source, asset.info);
  218. };
  219. /** @type {EntrypointDetails[]} */
  220. const entrypointsOverLimit = [];
  221. /** @type {string[]} */
  222. const entrypointsWithEmbeddedRuntime = [];
  223. for (const [name, entry] of compilation.entrypoints) {
  224. const size = getEntrypointSize(entry);
  225. if (size > /** @type {number} */ (entrypointSizeLimit)) {
  226. entrypointsOverLimit.push({
  227. name,
  228. size,
  229. files: entry.getFiles().filter(fileFilter)
  230. });
  231. isOverSizeLimitSet.add(entry);
  232. if (hasEmbeddedRuntime(compilation.chunkGraph, entry)) {
  233. entrypointsWithEmbeddedRuntime.push(name);
  234. }
  235. }
  236. }
  237. if (hints) {
  238. // 1. Individual Chunk: Size < 250kb
  239. // 2. Collective Initial Chunks [entrypoint] (Each Set?): Size < 250kb
  240. // 3. No Async Chunks
  241. // if !1, then 2, if !2 return
  242. if (assetsOverSizeLimit.length > 0) {
  243. addLargestModules(compilation, assetsOverSizeLimit);
  244. warnings.push(
  245. new AssetsOverSizeLimitWarning(
  246. assetsOverSizeLimit,
  247. /** @type {number} */ (assetSizeLimit)
  248. )
  249. );
  250. }
  251. if (entrypointsOverLimit.length > 0) {
  252. warnings.push(
  253. new EntrypointsOverSizeLimitWarning(
  254. entrypointsOverLimit,
  255. /** @type {number} */ (entrypointSizeLimit)
  256. )
  257. );
  258. }
  259. if (warnings.length > 0) {
  260. const someAsyncChunk = find(
  261. compilation.chunks,
  262. (chunk) => !chunk.canBeInitial()
  263. );
  264. if (!someAsyncChunk) {
  265. warnings.push(new NoAsyncChunksWarning());
  266. }
  267. if (entrypointsWithEmbeddedRuntime.length > 0) {
  268. warnings.push(
  269. new RuntimeInLargeChunkWarning(entrypointsWithEmbeddedRuntime)
  270. );
  271. }
  272. if (hints === "error") {
  273. compilation.errors.push(...warnings);
  274. } else if (hints === "stats") {
  275. compilation.hints.push(...warnings);
  276. } else {
  277. compilation.warnings.push(...warnings);
  278. }
  279. }
  280. }
  281. });
  282. }
  283. };