task-92: manualChunks 拆包后比较各页面首屏传输大小

This commit is contained in:
2026-08-30 23:07:08 +08:00
parent bd3af46cf8
commit eb158e76f0
2 changed files with 425 additions and 0 deletions
@@ -0,0 +1,236 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createChunkTransferReport } from '../src/shared/chunk-transfer-report.ts'
const plans = (items: Array<{ entry: string; chunks: string[] }>) => items
test('test_task_092_chunk_normal_default_path', () => {
const report = createChunkTransferReport({
entries: ['publish', 'dedupe', 'convert'],
plans: plans([
{ entry: 'publish', chunks: ['element-plus', 'vue-vendor', 'publish-page'] },
{ entry: 'dedupe', chunks: ['element-plus', 'vue-vendor', 'dedupe-page'] },
{ entry: 'convert', chunks: ['element-plus', 'vue-vendor', 'convert-page'] },
]),
chunkSizes: { 'element-plus': 300_000, 'vue-vendor': 80_000, 'publish-page': 20_000, 'dedupe-page': 25_000, 'convert-page': 30_000 },
entrySizes: { publish: 10_000, dedupe: 12_000, convert: 11_000 },
})
const reportEntries = report.entries
assert.equal(reportEntries.length, 3)
assert.deepEqual(
reportEntries.map((e) => e.entry),
['publish', 'dedupe', 'convert'],
'入口顺序稳定',
)
assert.equal(reportEntries[0].entryBytes, 10_000)
assert.equal(reportEntries[0].chunkBytes, 400_000)
assert.equal(reportEntries[0].transferBytes, 410_000)
assert.equal(reportEntries[0].chunkCount, 3)
// 公共 chunk 只统计一次
assert.equal(report.sharedBytes, 380_000, 'element-plus + vue-vendor 合计')
assert.equal(report.largest?.entry, 'convert')
assert.equal(report.largest?.transferBytes, 421_000)
assert.equal(report.smallest?.entry, 'publish')
assert.equal(report.totalBytes, 410_000 + 417_000 + 421_000)
})
test('test_task_092_chunk_normal_multiple_items', () => {
const entries = ['a', 'b', 'c', 'd', 'e']
const report = createChunkTransferReport({
entries,
plans: plans(entries.map((entry) => ({ entry, chunks: ['element-plus', `${entry}-page`] }))),
chunkSizes: {
'element-plus': 300_000,
'a-page': 10_000,
'b-page': 20_000,
'c-page': 30_000,
'd-page': 40_000,
'e-page': 50_000,
},
entrySizes: { a: 5_000, b: 6_000, c: 7_000, d: 8_000, e: 9_000 },
})
assert.equal(report.entries.length, 5)
// 批量结果不丢失、顺序稳定
assert.deepEqual(report.entries.map((e) => e.entry), entries)
for (let i = 0; i < entries.length; i++) {
const item = report.entries[i]
assert.equal(item.transferBytes, (i + 1) * 10_000 + 300_000 + (i + 5) * 1_000)
}
assert.equal(report.sharedBytes, 300_000, 'element-plus 出现在全部入口,只计一次')
assert.deepEqual(report.entries.map((e) => e.chunkCount), [2, 2, 2, 2, 2])
})
test('test_task_092_chunk_normal_repeated_operation_is_idempotent', () => {
const options = {
entries: ['publish', 'dedupe'],
plans: plans([
{ entry: 'publish', chunks: ['element-plus', 'publish-page'] },
{ entry: 'dedupe', chunks: ['element-plus', 'dedupe-page'] },
]),
chunkSizes: { 'element-plus': 300_000, 'publish-page': 20_000, 'dedupe-page': 25_000 },
entrySizes: { publish: 10_000, dedupe: 12_000 },
}
const once = createChunkTransferReport(options)
const twice = createChunkTransferReport(options)
assert.deepEqual(once.entries, twice.entries, '同一输入两次计算结果一致')
assert.equal(once.sharedBytes, twice.sharedBytes)
assert.equal(once.totalBytes, twice.totalBytes)
assert.deepEqual(once.largest, twice.largest)
assert.deepEqual(once.smallest, twice.smallest)
// 输入对象不被修改
assert.equal(options.chunkSizes['element-plus'], 300_000)
assert.equal(options.plans[0].chunks.length, 2)
})
test('test_task_092_chunk_boundary_empty_input', () => {
const report = createChunkTransferReport({ entries: [], plans: [], chunkSizes: {}, entrySizes: {} })
assert.deepEqual(report.entries, [])
assert.equal(report.sharedBytes, 0)
assert.equal(report.totalBytes, 0)
assert.equal(report.largest, undefined)
assert.equal(report.smallest, undefined)
})
test('test_task_092_chunk_boundary_single_item', () => {
const report = createChunkTransferReport({
entries: ['withdraw'],
plans: plans([{ entry: 'withdraw', chunks: ['element-plus', 'withdraw-page'] }]),
chunkSizes: { 'element-plus': 300_000, 'withdraw-page': 15_000 },
entrySizes: { withdraw: 8_000 },
})
assert.equal(report.entries.length, 1)
assert.equal(report.entries[0].transferBytes, 323_000)
assert.equal(report.sharedBytes, 0, 'chunk 只出现在一个入口时不算公共')
assert.equal(report.largest?.entry, 'withdraw')
assert.equal(report.smallest?.entry, 'withdraw')
assert.equal(report.largest?.transferBytes, report.smallest?.transferBytes)
})
test('test_task_092_chunk_boundary_limit_and_overflow', () => {
const report = createChunkTransferReport({
entries: ['a', 'b', 'c', 'd'],
plans: plans(
['a', 'b', 'c', 'd'].map((entry) => ({
entry,
chunks: ['element-plus', 'vue-vendor', 'shared', `${entry}-page`, `extra-${entry}`],
})),
),
chunkSizes: {
'element-plus': 300_000,
'vue-vendor': 80_000,
shared: 50_000,
'a-page': 10_000,
'b-page': 10_000,
'c-page': 10_000,
'd-page': 10_000,
'extra-a': 5_000,
'extra-b': 5_000,
'extra-c': 5_000,
'extra-d': 5_000,
},
entrySizes: { a: 5_000, b: 5_000, c: 5_000, d: 5_000 },
maxEntries: 2,
maxChunksPerEntry: 3,
})
assert.equal(report.entries.length, 2, '超过 maxEntries 只统计前 2 个入口')
assert.deepEqual(
report.entries.map((e) => e.entry),
['a', 'b'],
)
assert.equal(report.entries[0].chunkCount, 3, '每入口最多统计 3 个 chunk')
assert.equal(report.entries[0].chunkBytes, 430_000, 'element-plus + vue-vendor + shared')
assert.equal(report.entries[0].transferBytes, 435_000)
// 超出部分的 chunk 尺寸不计入
assert.ok(!('extra-a' in report.chunkBytesOf(report.entries[0])), '被截断的 chunk 不计入')
// 入口缺失 plan 或尺寸:fail-fast 抛错
assert.throws(
() => createChunkTransferReport({
entries: ['a', 'x'],
plans: plans([{ entry: 'a', chunks: ['element-plus'] }]),
chunkSizes: { 'element-plus': 100 },
entrySizes: { a: 1 },
}),
/缺少 entry 的拆包计划: x/,
)
assert.throws(
() => createChunkTransferReport({
entries: ['a'],
plans: plans([{ entry: 'a', chunks: ['element-plus'] }]),
chunkSizes: { 'element-plus': 100 },
entrySizes: {},
}),
/缺少 entry 尺寸: a/,
)
})
test('test_task_092_chunk_invalid_input_rejected', () => {
assert.throws(() => createChunkTransferReport({} as never), /entries 必须是数组/)
assert.throws(
() => createChunkTransferReport({ entries: ['a'], plans: 'x' as never, chunkSizes: {}, entrySizes: {} }),
/plans 必须是数组/,
)
assert.throws(
() => createChunkTransferReport({ entries: ['a'], plans: [], chunkSizes: null as never, entrySizes: {} }),
/chunkSizes 必须是对象/,
)
assert.throws(
() => createChunkTransferReport({ entries: ['a'], plans: [], chunkSizes: {}, entrySizes: null as never }),
/entrySizes 必须是对象/,
)
assert.throws(
() => createChunkTransferReport({ entries: ['a'], plans: [], chunkSizes: {}, entrySizes: new Date() as never }),
/entrySizes 必须是对象/,
)
assert.throws(
() => createChunkTransferReport({ entries: ['a'], plans: [], chunkSizes: {}, entrySizes: {}, maxEntries: 0 }),
/maxEntries 必须为正数/,
)
assert.throws(
() => createChunkTransferReport({ entries: ['a'], plans: [], chunkSizes: {}, entrySizes: {}, maxChunksPerEntry: -1 }),
/maxChunksPerEntry 必须为正数/,
)
// 尺寸为负数拒绝
assert.throws(
() => createChunkTransferReport({
entries: ['a'],
plans: plans([{ entry: 'a', chunks: ['c'] }]),
chunkSizes: { c: -1 },
entrySizes: { a: 1 },
}),
/尺寸不能为负数/,
)
// 缺失 chunk 尺寸拒绝
assert.throws(
() => createChunkTransferReport({
entries: ['a'],
plans: plans([{ entry: 'a', chunks: ['c'] }]),
chunkSizes: {},
entrySizes: { a: 1 },
}),
/缺少 chunk 尺寸: c/,
)
})
test('test_task_092_chunk_dependency_failure_releases_resources', () => {
let broken = true
const poisonedSizes = new Proxy({ 'element-plus': 300_000, 'a-page': 20_000 }, {
get(target, prop, receiver) {
if (broken && prop === 'element-plus') throw new Error('size getter down')
return Reflect.get(target, prop, receiver)
},
})
const options = {
entries: ['a'],
plans: plans([{ entry: 'a', chunks: ['element-plus', 'a-page'] }]),
chunkSizes: poisonedSizes as Record<string, number>,
entrySizes: { a: 5_000 },
}
// 依赖(尺寸读取)失败:计算抛错,不产生部分结果
assert.throws(() => createChunkTransferReport(options), /size getter down/)
// 错误可恢复:依赖恢复后同一输入计算成功,输入未被修改
broken = false
const report = createChunkTransferReport(options)
assert.equal(report.entries.length, 1)
assert.equal(report.entries[0].transferBytes, 325_000)
assert.deepEqual(options.plans[0].chunks, ['element-plus', 'a-page'])
})