mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
0156307f55
Follow-up to https://github.com/vercel/next.js/pull/95807. Makes payload size match prod sizes closer and aligns chunk count by adding a lot of client modules. I originally thought to unify them into `client.js` or such but maybe it's good to see how compilation behaves with many small files? Which is how real projects do that. ``` === dashboard.html (fixture) vs p-overview.html (real) total KB 665 637 rows (model/I/T/other) 258/111/3/1 240/125/5/27 byte share model/I/T % 43/57/0 36/62/2 median row size B 906 106 max depth 43 53 mean depth 8.9 10.5 elements per KB(model) 7.7 5.2 pure-data byte share % 18 21 client-ref elements % 35 32 objects:elements ratio 1.18 1.23 avg props per element 2.6 3.1 median children fanout 4 4 string bytes % 50 54 median string len B 12 15 row refs per KB 0.4 0.4 encoded scalars per KB 0.7 1.2 undefined markers 247 740 === docs.html (fixture) vs nextjs-docs.html (real) total KB 487 487 rows (model/I/T/other) 72/40/7/1 55/43/2/20 byte share model/I/T % 89/8/3 94/6/0 median row size B 199 340 max depth 27 32 mean depth 22.3 12.3 elements per KB(model) 0.6 0.8 pure-data byte share % 7 9 client-ref elements % 29 33 objects:elements ratio 8.67 4.92 avg props per element 2.0 2.8 median children fanout 3 4 string bytes % 47 64 median string len B 17 35 row refs per KB 0.2 0.1 encoded scalars per KB 7.4 2.9 undefined markers 3586 1377 === blog.html (fixture) vs vercel-blog.html (real) total KB 778 797 rows (model/I/T/other) 72/39/0/1 102/53/7/14 byte share model/I/T % 93/7/0 87/8/5 median row size B 145 505 max depth 34 44 mean depth 9.6 12.2 elements per KB(model) 0.7 1.1 pure-data byte share % 92 85 client-ref elements % 38 33 objects:elements ratio 31.19 16.39 avg props per element 1.9 2.7 median children fanout 3 4 string bytes % 46 48 median string len B 6 9 row refs per KB 0.1 0.1 encoded scalars per KB 0.4 0.6 undefined markers 242 426 ``` Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
148 lines
4.8 KiB
JavaScript
148 lines
4.8 KiB
JavaScript
// Generates a bulk client-side module graph (app/ui/vendor/, gitignored)
|
|
// plus synthetic route segments (app/g/, gitignored) that each use a
|
|
// different overlapping slice of it.
|
|
//
|
|
// Flight's client-reference import rows carry each module's transitive
|
|
// chunk closure. Turbopack's production chunker partitions modules by
|
|
// which route chunk-groups use them, so a small app with a handful of
|
|
// uniform routes merges everything into a few chunks regardless of code
|
|
// volume. Real deployments have hundreds of segments with heterogeneous
|
|
// imports, which is what produces closures of dozens of chunks repeated
|
|
// across every import row. The generated segments reproduce that shape at
|
|
// a moderate scale.
|
|
//
|
|
// The render-pipeline benchmark runs this automatically before building.
|
|
// To build bench/basic-app manually, run it once first:
|
|
// node bench/basic-app/scripts/generate-client-graph.mjs
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const VERSION = 'v5-120x24-40routes-w60-c120'
|
|
const MODULES = 120
|
|
const FUNCTIONS_PER_MODULE = 24
|
|
const ROWS_PER_TABLE = 18
|
|
const ROUTES = 40
|
|
const ROUTE_WINDOW = 60
|
|
const CORE_WINDOW = 120
|
|
|
|
const appDir = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'app'
|
|
)
|
|
const vendorDir = path.join(appDir, 'ui', 'vendor')
|
|
const routesDir = path.join(appDir, 'g')
|
|
const marker = path.join(vendorDir, '.generated')
|
|
|
|
if (fs.existsSync(marker) && fs.readFileSync(marker, 'utf8') === VERSION) {
|
|
process.exit(0)
|
|
}
|
|
|
|
fs.rmSync(vendorDir, { recursive: true, force: true })
|
|
fs.rmSync(routesDir, { recursive: true, force: true })
|
|
fs.mkdirSync(vendorDir, { recursive: true })
|
|
fs.mkdirSync(routesDir, { recursive: true })
|
|
|
|
for (let m = 0; m < MODULES; m++) {
|
|
const parts = [
|
|
"'use client';",
|
|
'// Generated module: part of the bulk client graph. See scripts/generate-client-graph.mjs.',
|
|
]
|
|
for (let f = 0; f < FUNCTIONS_PER_MODULE; f++) {
|
|
const rows = []
|
|
for (let r = 0; r < ROWS_PER_TABLE; r++) {
|
|
rows.push(
|
|
`{a:${r * 7 + 3},b:${r * 13 + m},m:999983,tag:'g${m}_${f}_${r}_${'x'.repeat(48)}'}`
|
|
)
|
|
}
|
|
parts.push(
|
|
`export function graph${m}_${f}(input) {
|
|
const table = [${rows.join(',')}]
|
|
let acc = typeof input === 'number' ? input : String(input).length
|
|
for (const t of table) acc = (acc * t.a + t.b) % t.m
|
|
return {acc, tags: table.length}
|
|
}`
|
|
)
|
|
}
|
|
fs.writeFileSync(path.join(vendorDir, `graph${m}.js`), parts.join('\n'))
|
|
}
|
|
|
|
// Entry used by the committed fixtures: the "core" shared slice.
|
|
const index = [
|
|
"'use client';",
|
|
...Array.from(
|
|
{ length: CORE_WINDOW },
|
|
(_, i) => `import {graph${i}_0} from './graph${i}'`
|
|
),
|
|
`const PROBES = [${Array.from({ length: CORE_WINDOW }, (_, i) => `graph${i}_0`).join(', ')}]`,
|
|
`export function graphProbe(seed) {
|
|
return PROBES[Math.abs(seed | 0) % PROBES.length](seed).acc
|
|
}`,
|
|
]
|
|
fs.writeFileSync(path.join(vendorDir, 'index.js'), index.join('\n'))
|
|
|
|
// Per-fixture foundation slices: the docs and blog routes' client
|
|
// components sit on differently-sized subsets of the shared graph, the
|
|
// way different surfaces of a deployment share different vendor layers.
|
|
for (const [name, size] of [
|
|
['docs', 20],
|
|
['blog', 35],
|
|
]) {
|
|
fs.writeFileSync(
|
|
path.join(vendorDir, `slice-${name}.js`),
|
|
[
|
|
"'use client';",
|
|
...Array.from(
|
|
{ length: size },
|
|
(_, i) => `import {graph${i}_0} from './graph${i}'`
|
|
),
|
|
`const PROBES = [${Array.from({ length: size }, (_, i) => `graph${i}_0`).join(', ')}]`,
|
|
`export function ${name}SliceProbe(seed) {
|
|
return PROBES[Math.abs(seed | 0) % PROBES.length](seed).acc
|
|
}`,
|
|
].join('\n')
|
|
)
|
|
}
|
|
|
|
// Synthetic segments: each uses a different overlapping window of the
|
|
// graph, giving modules distinct route-usage signatures.
|
|
for (let r = 0; r < ROUTES; r++) {
|
|
const dir = path.join(routesDir, `r${r}`)
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
const start = Math.floor(
|
|
(r * (MODULES - ROUTE_WINDOW)) / Math.max(1, ROUTES - 1)
|
|
)
|
|
const mods = Array.from({ length: ROUTE_WINDOW }, (_, i) => start + i)
|
|
fs.writeFileSync(
|
|
path.join(dir, 'widget.js'),
|
|
[
|
|
"'use client';",
|
|
...mods.map(
|
|
(m) => `import {graph${m}_0} from '../../ui/vendor/graph${m}'`
|
|
),
|
|
`const PROBES = [${mods.map((m) => `graph${m}_0`).join(', ')}]`,
|
|
`export default function Widget() {
|
|
return <div data-route="r${r}">{PROBES.length}</div>
|
|
}`,
|
|
].join('\n')
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(dir, 'page.js'),
|
|
[
|
|
`import Widget from './widget'`,
|
|
'',
|
|
`export const dynamic = 'force-dynamic'`,
|
|
'',
|
|
`export default function Page() {
|
|
return <Widget />
|
|
}`,
|
|
].join('\n')
|
|
)
|
|
}
|
|
|
|
fs.writeFileSync(marker, VERSION)
|
|
console.log(
|
|
`generated ${MODULES} graph modules and ${ROUTES} segments in bench/basic-app`
|
|
)
|