UNPKG

next

Version:

The React Framework

267 lines (266 loc) • 14 kB
const PLUGIN_NAME = 'CssChunkingPlugin'; /** * Merge chunks until they are bigger than the target size. */ const MIN_CSS_CHUNK_SIZE = 30 * 1024; /** * Avoid merging chunks when they would be bigger than this size. */ const MAX_CSS_CHUNK_SIZE = 100 * 1024; function isGlobalCss(module) { return !/\.module\.(css|scss|sass)$/.test(module.nameForCondition() || ''); } export class CssChunkingPlugin { constructor(strict){ this.strict = strict; } apply(compiler) { const strict = this.strict; const summary = !!process.env.CSS_CHUNKING_SUMMARY; compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation)=>{ let once = false; compilation.hooks.optimizeChunks.tap({ name: PLUGIN_NAME, stage: 5 }, ()=>{ if (once) { return; } once = true; const chunkGraph = compilation.chunkGraph; let changed = undefined; const chunkStates = new Map(); const chunkStatesByModule = new Map(); // Collect all css modules in chunks and the execpted order of them for (const chunk of compilation.chunks){ var _chunk_name; if ((_chunk_name = chunk.name) == null ? void 0 : _chunk_name.startsWith('pages/')) continue; const modules = []; for (const module of chunkGraph.getChunkModulesIterable(chunk)){ var _module_type; if (!((_module_type = module.type) == null ? void 0 : _module_type.startsWith('css'))) continue; modules.push(module); } if (!modules.length) continue; const chunkState = { chunk, modules, order: 0, requests: modules.length }; chunkStates.set(chunk, chunkState); for(let i = 0; i < modules.length; i++){ const module = modules[i]; let moduleChunkStates = chunkStatesByModule.get(module); if (!moduleChunkStates) { moduleChunkStates = new Map(); chunkStatesByModule.set(module, moduleChunkStates); } moduleChunkStates.set(chunkState, i); chunkStatesByModule.set(module, moduleChunkStates); } } // Sort modules by their index sum const orderedModules = []; for (const [module, moduleChunkStates] of chunkStatesByModule){ let sum = 0; for (const i of moduleChunkStates.values()){ sum += i; } orderedModules.push({ module, sum }); } orderedModules.sort((a, b)=>a.sum - b.sum); // A queue of modules that still need to be processed const remainingModules = new Set(orderedModules.map(({ module })=>module)); // In loose mode we guess the dependents of modules from the order // assuming that when a module is a dependency of another module // it will always appear before it in every chunk. const allDependents = new Map(); if (!this.strict) { for (const b of remainingModules){ const dependent = new Set(); loop: for (const a of remainingModules){ if (a === b) continue; // check if a depends on b for (const [chunkState, ia] of chunkStatesByModule.get(a)){ const bChunkStates = chunkStatesByModule.get(b); const ib = bChunkStates.get(chunkState); if (ib === undefined) { continue loop; } if (ib > ia) { continue loop; } } dependent.add(a); } if (dependent.size > 0) allDependents.set(b, dependent); } } // Stores the new chunk for every module const newChunksByModule = new Map(); // Process through all modules for (const startModule of remainingModules){ let globalCssMode = isGlobalCss(startModule); // The current position of processing in all selected chunks let allChunkStates = new Map(chunkStatesByModule.get(startModule)); // The list of modules that goes into the new chunk const newChunkModules = new Set([ startModule ]); // The current size of the new chunk let currentSize = startModule.size(); // A pool of potential modules where the next module is selected from. // It's filled from the next module of the selected modules in every chunk. // It also keeps some metadata to improve performance [size, chunkStates]. const potentialNextModules = new Map(); for (const [chunkState, i] of allChunkStates){ const nextModule = chunkState.modules[i + 1]; if (nextModule && remainingModules.has(nextModule)) { potentialNextModules.set(nextModule, [ nextModule.size(), chunkStatesByModule.get(nextModule) ]); } } // Try to add modules to the chunk until a break condition is met let cont; do { cont = false; // We try to select a module that reduces request count and // has the highest number of requests const orderedPotentialNextModules = []; for (const [nextModule, [size, nextChunkStates]] of potentialNextModules){ let maxRequests = 0; for (const chunkState of nextChunkStates.keys()){ // There is always some overlap if (allChunkStates.has(chunkState)) { maxRequests = Math.max(maxRequests, chunkState.requests); } } orderedPotentialNextModules.push([ nextModule, size, nextChunkStates, maxRequests ]); } orderedPotentialNextModules.sort((a, b)=>b[3] - a[3] || (a[0].identifier() < b[0].identifier() ? -1 : 1)); // Try every potential module loop: for (const [nextModule, size, nextChunkStates] of orderedPotentialNextModules){ if (currentSize + size > MAX_CSS_CHUNK_SIZE) { continue; } if (!strict) { // In loose mode we only check if the dependencies are not violated const dependent = allDependents.get(nextModule); if (dependent) { for (const dep of dependent){ if (newChunkModules.has(dep)) { continue loop; } } } } else { // In strict mode we check that none of the order in any chunk is changed by adding the module for (const [chunkState, i] of nextChunkStates){ const prevState = allChunkStates.get(chunkState); if (prevState === undefined) { // New chunk group, can add it, but should we? // We only add that if below min size if (currentSize < MIN_CSS_CHUNK_SIZE) { continue; } else { continue loop; } } else if (prevState + 1 === i) { continue; } else { continue loop; } } } // Global CSS must not leak into unrelated chunks const nextIsGlobalCss = isGlobalCss(nextModule); if (nextIsGlobalCss && globalCssMode) { if (allChunkStates.size !== nextChunkStates.size) { continue; } } if (globalCssMode) { for (const chunkState of nextChunkStates.keys()){ if (!allChunkStates.has(chunkState)) { continue loop; } } } if (nextIsGlobalCss) { for (const chunkState of allChunkStates.keys()){ if (!nextChunkStates.has(chunkState)) { continue loop; } } } potentialNextModules.delete(nextModule); currentSize += size; if (nextIsGlobalCss) { globalCssMode = true; } for (const [chunkState, i] of nextChunkStates){ if (allChunkStates.has(chunkState)) { // This reduces the request count of the chunk group chunkState.requests--; } allChunkStates.set(chunkState, i); const newNextModule = chunkState.modules[i + 1]; if (newNextModule && remainingModules.has(newNextModule) && !newChunkModules.has(newNextModule)) { potentialNextModules.set(newNextModule, [ newNextModule.size(), chunkStatesByModule.get(newNextModule) ]); } } newChunkModules.add(nextModule); cont = true; break; } }while (cont); const newChunk = compilation.addChunk(); newChunk.preventIntegration = true; newChunk.idNameHints.add('css'); for (const module of newChunkModules){ remainingModules.delete(module); chunkGraph.connectChunkAndModule(newChunk, module); newChunksByModule.set(module, newChunk); } changed = true; } for (const { chunk, modules } of chunkStates.values()){ const chunks = new Set(); for (const module of modules){ const newChunk = newChunksByModule.get(module); if (newChunk) { chunkGraph.disconnectChunkAndModule(chunk, module); if (chunks.has(newChunk)) continue; chunks.add(newChunk); chunk.split(newChunk); } } } if (summary) { console.log('Top 20 chunks by request count:'); const orderedChunkStates = [ ...chunkStates.values() ]; orderedChunkStates.sort((a, b)=>b.requests - a.requests); for (const { chunk, modules, requests } of orderedChunkStates.slice(0, 20)){ console.log(`- ${requests} requests for ${chunk.name} (has ${modules.length} modules)`); } } return changed; }); }); } } //# sourceMappingURL=css-chunking-plugin.js.map