add plugin to avoid too many css requests (#62530)
### What? Reduces the number of css chunks per entrypoint by merging chunks. ### Why? In larger application the total CSS deduplication approach falls short in a sense of it's causing way too many separate chunks, which results in way to many requests. That affects page load performance. Closes PACK-2598
This commit is contained in:
parent
d9b2a4baea
commit
110fc47f7c
2 changed files with 82 additions and 0 deletions
|
@ -83,6 +83,7 @@ import {
|
||||||
createAppRouterApiAliases,
|
createAppRouterApiAliases,
|
||||||
} from './create-compiler-aliases'
|
} from './create-compiler-aliases'
|
||||||
import { hasCustomExportOutput } from '../export/utils'
|
import { hasCustomExportOutput } from '../export/utils'
|
||||||
|
import { MergeCssChunksPlugin } from './webpack/plugins/merge-css-chunks-plugin'
|
||||||
|
|
||||||
type ExcludesFalse = <T>(x: T | false) => x is T
|
type ExcludesFalse = <T>(x: T | false) => x is T
|
||||||
type ClientEntries = {
|
type ClientEntries = {
|
||||||
|
@ -1877,6 +1878,7 @@ export default async function getBaseWebpackConfig(
|
||||||
new NextFontManifestPlugin({
|
new NextFontManifestPlugin({
|
||||||
appDir,
|
appDir,
|
||||||
}),
|
}),
|
||||||
|
!dev && isClient && new MergeCssChunksPlugin(),
|
||||||
!dev &&
|
!dev &&
|
||||||
isClient &&
|
isClient &&
|
||||||
new (require('./webpack/plugins/telemetry-plugin').TelemetryPlugin)(
|
new (require('./webpack/plugins/telemetry-plugin').TelemetryPlugin)(
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import type { Chunk, ChunkGraph, Compiler } from 'webpack'
|
||||||
|
|
||||||
|
const PLUGIN_NAME = 'MergeCssChunksPlugin'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Css chunks smaller than this size will be merged with other css chunks.
|
||||||
|
*/
|
||||||
|
const MIN_CSS_CHUNK_SIZE = 30 * 1024
|
||||||
|
/**
|
||||||
|
* When merging css chunks it will select an number N where the total size is just bigger than this size.
|
||||||
|
* Exception: N must be at least 2 even if the size is already bigger than this size.
|
||||||
|
*/
|
||||||
|
const TARGET_CSS_CHUNK_SIZE = 60 * 1024
|
||||||
|
/**
|
||||||
|
* When an entrypoint has more css chunks than this number it merge the smallest ones to try to stay below that number.
|
||||||
|
* Exception: When there are more than twice as much css chunks that larger than MAX_CSS_CHUNK_SIZE it will only half the number of css chunks.
|
||||||
|
*/
|
||||||
|
const MAX_CSS_CHUNKS = 15
|
||||||
|
|
||||||
|
function isCssChunk(chunkGraph: ChunkGraph, chunk: Chunk): boolean {
|
||||||
|
for (const mod of chunkGraph.getChunkModulesIterable(chunk)) {
|
||||||
|
return mod.type.startsWith('css')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MergeCssChunksPlugin {
|
||||||
|
public apply(compiler: Compiler) {
|
||||||
|
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
||||||
|
let once = false
|
||||||
|
compilation.hooks.optimizeChunks.tap(
|
||||||
|
{
|
||||||
|
name: PLUGIN_NAME,
|
||||||
|
stage: 20,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (once) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
once = true
|
||||||
|
const chunkGraph = compilation.chunkGraph
|
||||||
|
let changed = false
|
||||||
|
for (const [, entrypoint] of compilation.entrypoints) {
|
||||||
|
const cssChunks = entrypoint.chunks
|
||||||
|
.filter((chunk) => isCssChunk(chunkGraph, chunk))
|
||||||
|
.map((chunk) => [chunk, chunkGraph.getChunkSize(chunk)] as const)
|
||||||
|
cssChunks.sort((a, b) => b[1] - a[1])
|
||||||
|
|
||||||
|
// We want have at most MAX_CSS_CHUNKS chunks.
|
||||||
|
// When we start merging at startMergingIndex this would half the number of chunks after that index.
|
||||||
|
const startMergingIndex = 2 * MAX_CSS_CHUNKS - cssChunks.length
|
||||||
|
|
||||||
|
// We select small chunks and chunks after the index
|
||||||
|
const selectedCssChunks = cssChunks.filter(
|
||||||
|
([, size], i) =>
|
||||||
|
size < MIN_CSS_CHUNK_SIZE || i >= startMergingIndex
|
||||||
|
)
|
||||||
|
while (selectedCssChunks.length >= 2) {
|
||||||
|
const [biggest, bigSize] = selectedCssChunks.shift()!
|
||||||
|
let mergedSize = bigSize
|
||||||
|
do {
|
||||||
|
const [smallest, size] = selectedCssChunks.pop()!
|
||||||
|
if (chunkGraph.canChunksBeIntegrated(biggest, smallest)) {
|
||||||
|
chunkGraph.integrateChunks(biggest, smallest)
|
||||||
|
compilation.chunks.delete(smallest)
|
||||||
|
mergedSize += size
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} while (
|
||||||
|
selectedCssChunks.length > 0 &&
|
||||||
|
mergedSize < TARGET_CSS_CHUNK_SIZE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue