web-font/scripts/test-leafer-million-baseline.mts
崮生(子虚) 0ab51e496c perf(leafer-x-webfont): 注册回调贡献者回记+静默链应用,百万节点打字场景零风暴
问题(1M Text 节点实测):
- v0 字体注册回调里全树改写 fontFamily:百万次 property.change 污染
  dirty 集,打字场景退化 20s;单点改动 978ms
- 公共 setter 路径(v2):百万次完整属性变更管线 = 2.2s 风暴

方案(v4):
- 贡献者回记:commit 时合并累加每个 family 的贡献节点(不覆盖,
  迟到 chunk 仍能找到贡献者),chunk 回调只把贡献者标脏——链应用
  范围从全树缩小到实际用字节点,打字场景 O(1)
- 静默/公共写分流:溢出(初始全量等贡献者>512)走静默写 __ 数据层
  (~50ns/次,不触发事件不污染 dirty);少量贡献者走公共 setter
  (leafer 自动局部重绘该节点)
- ready() 语义闭环:字体全部落地后同步消化残留 dirtyAll/renderPending,
  否则会被下一个不相关的用户操作捡走——表现为「改一个字卡 800ms」
- forceRender 收敛:仅静默写/浏览器模式标记 renderPending,flush 尾部
  无 in-flight chunk 时合并成一次执行

实测(1M Text,真实 leafer + 本地后端):
- 单点改动 flush: 856ms → 0.15ms
- 随机100节点 flush: 0.9ms
- 新增 Text: 0.08ms
- 打字同步按键成本: 0.2ms/次(与无插件基线 0.012ms 同数量级)
- 拖拽等无文字变化操作: 0 请求 0 提交
2026-08-15 21:13:07 +08:00

51 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 归因对照:百万节点下连续赋值 text 的成本,插件开 vs 关
*
* 疑问压测里「连续打字50次」1M 节点耗时 ~3s需确认这是 leafer 自身
* settertext 改动 → 布局失效)的固有成本,还是插件监听引入的。
*
* 运行npx tsx scripts/test-leafer-million-baseline.mts
*/
import { Leafer, Text, Group, useCanvas } from '@leafer-ui/node'
import { Canvas as NapiCanvas, loadImage } from '@napi-rs/canvas'
import { performance } from 'node:perf_hooks'
useCanvas('napi', { Canvas: NapiCanvas, loadImage })
const POOL = '静心茶舍天地玄黄宇宙洪荒'
async function buildTree(total: number) {
const leafer = new Leafer({ width: 800, height: 600, fill: '#fff' })
const GROUPS = 100
const PER = Math.floor(total / GROUPS)
const allTexts: InstanceType<typeof Text>[] = []
for (let g = 0; g < GROUPS; g++) {
const group = new Group()
for (let i = 0; i < PER; i++) {
const t = new Text({ text: POOL[i % POOL.length]!, fontFamily: 'sans-serif', fontSize: 12, fill: '#000', x: (i % 80) * 10, y: Math.floor(i / 80) * 14 })
group.add(t)
allTexts.push(t)
}
leafer.add(group)
}
return { leafer, allTexts }
}
async function main(): Promise<void> {
const TOTAL = 1_000_000
console.log(`=== 无插件基线:${TOTAL.toLocaleString()} 节点下 50 次 text 赋值 ===`)
const { leafer, allTexts } = await buildTree(TOTAL)
const victim = allTexts[Math.floor(allTexts.length / 2)]!
await new Promise((r) => setTimeout(r, 200))
const t0 = performance.now()
for (let i = 0; i < 50; i++) {
victim.text = '连续打字测试' + POOL[i % POOL.length]! + i
}
const noPluginMs = performance.now() - t0
console.log(`[无插件] 50 次赋值: ${noPluginMs.toFixed(1)}ms (${(noPluginMs / 50).toFixed(1)}ms/次)`)
leafer.destroy()
}
await main()