| | import type { Chunk, Compiler, Module } from 'webpack' |
| |
|
| | const PLUGIN_NAME = 'CssChunkingPlugin' |
| |
|
| | |
| | |
| | |
| | const MIN_CSS_CHUNK_SIZE = 30 * 1024 |
| | |
| | |
| | |
| | const MAX_CSS_CHUNK_SIZE = 100 * 1024 |
| |
|
| | function isGlobalCss(module: Module) { |
| | return !/\.module\.(css|scss|sass)$/.test(module.nameForCondition() || '') |
| | } |
| |
|
| | type ChunkState = { |
| | chunk: Chunk |
| | modules: Module[] |
| | order: number |
| | requests: number |
| | } |
| |
|
| | export class CssChunkingPlugin { |
| | private strict: boolean |
| | constructor(strict: boolean) { |
| | this.strict = strict |
| | } |
| |
|
| | public apply(compiler: 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 | true = undefined |
| |
|
| | const chunkStates = new Map<Chunk, ChunkState>() |
| | const chunkStatesByModule = new Map<Module, Map<ChunkState, number>>() |
| |
|
| | |
| | for (const chunk of compilation.chunks) { |
| | if (chunk.name?.startsWith('pages/')) continue |
| | const modules = [] |
| | for (const module of chunkGraph.getChunkModulesIterable(chunk)) { |
| | if (!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) |
| | } |
| | } |
| |
|
| | |
| | const orderedModules: { module: Module; sum: number }[] = [] |
| |
|
| | 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) |
| |
|
| | |
| | const remainingModules = new Set( |
| | orderedModules.map(({ module }) => module) |
| | ) |
| |
|
| | |
| | |
| | |
| | const allDependents = new Map<Module, Set<Module>>() |
| |
|
| | if (!this.strict) { |
| | for (const b of remainingModules) { |
| | const dependent = new Set<Module>() |
| | loop: for (const a of remainingModules) { |
| | if (a === b) continue |
| | |
| | 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) |
| | } |
| | } |
| |
|
| | |
| | const newChunksByModule = new Map<Module, Chunk>() |
| |
|
| | |
| | for (const startModule of remainingModules) { |
| | let globalCssMode = isGlobalCss(startModule) |
| |
|
| | |
| | let allChunkStates = new Map(chunkStatesByModule.get(startModule)!) |
| |
|
| | |
| | const newChunkModules = new Set([startModule]) |
| |
|
| | |
| | let currentSize = startModule.size() |
| |
|
| | |
| | |
| | |
| | const potentialNextModules = new Map< |
| | Module, |
| | [number, Map<ChunkState, number>] |
| | >() |
| | 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)!, |
| | ]) |
| | } |
| | } |
| |
|
| | |
| | let cont |
| | do { |
| | cont = false |
| | |
| | |
| | const orderedPotentialNextModules = [] |
| | for (const [ |
| | nextModule, |
| | [size, nextChunkStates], |
| | ] of potentialNextModules) { |
| | let maxRequests = 0 |
| | for (const chunkState of nextChunkStates.keys()) { |
| | |
| | if (allChunkStates.has(chunkState)) { |
| | maxRequests = Math.max(maxRequests, chunkState.requests) |
| | } |
| | } |
| |
|
| | orderedPotentialNextModules.push([ |
| | nextModule, |
| | size, |
| | nextChunkStates, |
| | maxRequests, |
| | ] as const) |
| | } |
| | orderedPotentialNextModules.sort( |
| | (a, b) => |
| | b[3] - a[3] || |
| | (a[0].identifier() < b[0].identifier() ? -1 : 1) |
| | ) |
| |
|
| | |
| | loop: for (const [ |
| | nextModule, |
| | size, |
| | nextChunkStates, |
| | ] of orderedPotentialNextModules) { |
| | if (currentSize + size > MAX_CSS_CHUNK_SIZE) { |
| | |
| | continue |
| | } |
| | if (!strict) { |
| | |
| | const dependent = allDependents.get(nextModule) |
| | if (dependent) { |
| | for (const dep of dependent) { |
| | if (newChunkModules.has(dep)) { |
| | |
| | continue loop |
| | } |
| | } |
| | } |
| | } else { |
| | |
| | for (const [chunkState, i] of nextChunkStates) { |
| | const prevState = allChunkStates.get(chunkState) |
| | if (prevState === undefined) { |
| | |
| | |
| | if (currentSize < MIN_CSS_CHUNK_SIZE) { |
| | continue |
| | } else { |
| | continue loop |
| | } |
| | } else if (prevState + 1 === i) { |
| | |
| | continue |
| | } else { |
| | |
| | continue loop |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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)) { |
| | |
| | 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 |
| | } |
| | ) |
| | }) |
| | } |
| | } |
| |
|