- 性能测试
- 开发者工具
【免费下载链接】js-framework-benchmark
A comparison of the performance of a few popular javascript frameworks
导读
本文围绕 js-framework-benchmark 仓库中 frameworks/keyed/domphy 的 keyed 实现展开,系统讲解 Domphy 这一"无 JSX、无虚拟 DOM"的 patch-based 通用 UI 运行时在真实基准场景中的惯用法:如何用单个toState状态驱动整张表格、如何为每行建立独立标签状态、如何通过元素描述符(element descriptor)缓存让重排/删除时未变化的行跳过 patch。读完本文,你将掌握 Domphy 的核心状态模型、keyed 协调器的工作原理,以及如何在 js-framework-benchmark 框架中完成一次可复现的基准构建与运行。
一、Domphy 是什么:README 中的核心定位
仓库中的 frameworks/keyed/domphy/README.md 给出了 Domphy 的一锤定音式定位:
Domphy is a patch-based, framework-agnostic UI runtime. No JSX, no virtual DOM: UIs are plain objects keyed by HTML tag and reactivity is listener-based (
toState).
这段描述可以拆解为四个关键事实:
- Patch-based(基于补丁):Domphy 直接在真实 DOM 上执行最小化的补丁操作,而不是像 React/Vue 那样先构建虚拟 DOM 树再 diff;
- Framework-agnostic:它不绑定任何 JSX 或模板编译链,视图就是普通的 JavaScript 对象字面量;
- UI 是"以 HTML 标签为 key 的普通对象":例如
{ tr: [...], class: ... }即描述一个<tr>元素,对象属性名(tr、td、a、span)直接映射到标签名,属性值即子节点或属性; - 响应式基于监听器:状态由
toState(...)创建,读取时建立依赖(订阅),写入时触发订阅者重新执行对应的补丁逻辑。
此外,README 还明确了该实现采用"框架惯用的细粒度风格(idiomatic fine-grained style)",并列出四条设计要点,这四条要点在 src/main.ts 中均有对应实现,是理解整篇文章的骨架:
- 用一个keyed 的
State<Row[]>承载行列表,每行的_key驱动 keyed 协调器; - 每行拥有独立的 label 状态,使"更新每第 10 行"只重渲染这些行的文本;
- 一个表格级别的
selectedid 状态,每行从它派生出dangerclass; - 每行的元素描述符只创建一次并跨列表重渲染复用,因此重排/删除时未变化的行会被跳过。
二、目录与构建管线:从 TypeScript 源码到 dist/main.js
该实现的完整目录结构如下:
frameworks/keyed/domphy/ ├── README.md # 实现说明(本文所依据的核心文档) ├── index.html # 基准页面外壳 ├── package.json # 依赖与构建脚本 ├── package-lock.json └── src/ └── main.ts # 全部实现代码(唯一源码入口)从 package.json 可以看到:
- 运行时依赖
@domphy/core(版本^0.21.1),package.json中的js-framework-benchmark.frameworkVersionFromPackage指向它,基准结果中的版本号domphy-v0.21.1-keyed即由此推导; - 开发依赖
esbuild(版本0.25.12)负责打包; - 项目被标记为 TypeScript 语言实现,
main指向dist/main.js。
README 给出的构建命令是两步:
npm install npm run build-prod其背后对应package.json的两个脚本:
"dev": "esbuild src/main.ts --bundle --format=iife --outfile=dist/main.js", "build-prod": "esbuild src/main.ts --bundle --minify --format=iife --outfile=dist/main.js"二者的差别仅在于build-prod增加了--minify。两个脚本共同点:--bundle将@domphy/core依赖内联进单一文件,--format=iife生成可直接以<script>标签加载的立即执行函数,输出到dist/main.js。这正是 index.html 中<script src='dist/main.js'></script>所引用的产物,页面只需一个#main挂载点。
三、状态模型:一个列表 State + 一个选中 State
在 src/main.ts 中,全部应用状态只有两个:
const data = toState<Row[]>([]); const selected = toState<number | null>(null);data是带 key 的列表状态,元素类型为Row;selected是表格级别的选中行 id 状态,初始为null。
Row的定义(src/main.ts)值得注意——它把"行 id"与"标签状态"分开:
interface Row { id: number; label: ReturnType<typeof toState<string>>; }每行内部嵌套一个独立的label: toState<string>(...),这正是 README 第 2 条"per-row label states"的直接体现。buildData(src/main.ts)在创建行时,用三组词表(形容词、颜色、名词)随机拼接生成标签文本,并递增全局nextId作为行 id。
四、六个基准操作:每个操作如何触碰状态
js-framework-benchmark 的 keyed 基准要求实现 6 类操作,main.ts中每个操作都极其精简,因为它们只是对两个状态的读写:
| 操作 | 函数 | 实现要点(src/main.ts 行号) |
|---|---|---|
| Create 1k / Create 10k | run(count) | selected.set(null)后整体替换data.set(buildData(count))(L115-L118) |
| Append 1k | add(count) | 取当前数组 concat 新数据后再整体 set(L120-L122) |
| Update every 10th | update() | 步长 10 遍历,只对rows[i].label调用set(label + " !!!")(L124-L130) |
| Clear | clear() | 清空选中态与数据(L132-L135) |
| Remove one | remove(row) | 若删除的是选中行先清selected,再 filter 掉该行(L137-L140) |
| Swap rows | swapRows() | 行数少于 999 直接返回,交换next[1]与next[998]后整体 set(L142-L150) |
这里最值得展开的是update():它没有触碰data列表,而是逐行修改每行内嵌的label状态。由于每行元素描述符中的标签文本是(l: any) => row.label.get(l)(见下文第五节),只有被修改的 100 行(每第 10 行)对应的订阅者会被触发并执行 DOM 文本补丁,其余 900 行完全不参与,这正是 README 强调的"update every 10th row re-renders only those rows' text"。
同理,remove()与swapRows()走的是列表整体替换路径,此时 keyed 协调器依据每行的_key计算差异(见第五节),而不是粗暴地重建整个<tbody>。
五、元素描述符:一次创建、跨重渲染复用
README 第 4 条是这套实现最精巧的部分:"row element descriptors are created once per row and reused across list re-renders, so unchanged rows skip patching on reorder/remove."
在 Domphy 中,视图就是普通对象。例如删除按钮图标被定义为模块级常量(src/main.ts):
const REMOVE_ICON: DomphyElement = { span: null, class: "glyphicon glyphicon-remove", "aria-hidden": "true", } as DomphyElement;每行的元素描述符则由elementFor(row)(src/main.ts)构建,并用一个WeakMap<Row, DomphyElement>(elementCache)缓存:
const elementCache = new WeakMap<Row, DomphyElement>(); function elementFor(row: Row): DomphyElement { let element = elementCache.get(row); if (element) return element; element = { tr: [ { td: row.id, class: "col-md-1" }, { td: [ { a: (l: any) => row.label.get(l), onClick: () => selected.set(row.id), }, ], class: "col-md-4", }, { td: [ { a: [REMOVE_ICON], onClick: () => remove(row), }, ], class: "col-md-1", }, { td: null, class: "col-md-6" }, ], _key: row.id, class: (l: any) => (selected.get(l) === row.id ? "danger" : ""), } as DomphyElement; elementCache.set(row, element); return element; }这段代码同时示范了 Domphy 描述符的全部形态:
- 标签即 key:
tr、td、a、span直接作为对象键; - 静态文本/子元素直接写:
{ td: row.id, class: "col-md-1" }表示单元格内容是静态的 id 文本; - 动态内容用函数:
a: (l: any) => row.label.get(l)是一个读取表达式——它在首次求值时建立label状态与 DOM 文本节点的订阅关系,之后label.set(...)只触发这一处补丁;参数l是 Domphy 在求值时传入的上下文引用; - 事件直接挂在描述符上:
onClick: () => selected.set(row.id); _key驱动 keyed 协调器:_key: row.id让列表 diff 时能够精确匹配新旧行。注意行 id 与 label 状态都封装在Row对象里,因此即使data.set()传入全新数组,只要Row对象引用不变,elementCache仍能命中同一个描述符,未变化的行在重排/删除时直接被协调器跳过;- 派生 class 也用读取表达式:
class: (l: any) => (selected.get(l) === row.id ? "danger" : "")——选中状态变化时,只有相关行的 class 补丁会被执行。
elementCache选用WeakMap也很有讲究:key 是Row对象,当行从列表移除且无其他引用时,其描述符可被垃圾回收,避免长期占用内存,这对内存类基准(见第七节)是有利的设计。
六、页面脚手架与挂载
页面结构与 krausest 系列实现的 index.html 保持一致(注释中明确说明 DOM 结构 mirror 自frameworks/keyed/vanillajs):
tr > td.col-md-1 (id) + td.col-md-4 > a (label) + td.col-md-1 > a > span.glyphicon-remove + td.col-md-6 选中行:tr.dangersrc/main.ts 的actionButton(id, label, onClick)是操作按钮的工厂函数,它把按钮放进col-sm-6 smallpad容器,并挂上基准约定的元素 id(run、runlots、add、update、clear、swaprows)——这些 id 是 webdriver-ts 驱动脚本点击按钮的定位依据。
顶层App描述符(src/main.ts)由三部分组成:jumbotron 标题区、工具栏按钮区,以及核心的表格:
{ table: [ { tbody: (l: any) => data.get(l).map(elementFor), id: "tbody", }, ], class: "table table-hover table-striped test-data", },tbody的内容是列表读取表达式:data.get(l).map(elementFor)。列表状态每次set后,这个表达式会重新求值,Domphy 的 keyed 协调器依据每个描述符的_key与旧列表做最小化 diff——新增行插入、删除行移除、重排行移动,其余行保持原 DOM 节点不动。
最后一行完成挂载:
new ElementNode(App).render(document.getElementById("main")!);src/main.ts 使用@domphy/core导出的ElementNode将App描述符渲染进 index.html 中的<div id='main'></div>。页面还通过<link href="/css/currentStyle.css">引入仓库根目录 css/currentStyle.css,保证 Bootstrap 风格表格与按钮的样式与基准一致。
七、基准中的参与情况与产物规模
在仓库的 webdriver-ts/results.json 中,可以确认该实现以domphy-v0.21.1-keyed的名义参与了基准测试,共覆盖 15 个 benchmark 条目:
01_run1k, 02_replace1k, 03_update10th1k_x16, 04_select1k, 05_swap1k, 06_remove-one-1k, 07_create10k, 08_create1k-after1k_x2, 09_clear1k_x8, 21_ready-memory, 22_run-memory, 25_run-clear-memory, 41_size-uncompressed, 42_size-compressed, 43_first-paint从中可以观察到基准对实现的两类要求:
- 交互类(01-09)逐一对应第五节的 6 个操作,外加 select、replace 与"create-after-clear"等组合场景;
- 内存类(21/22/25)与体积类(41/42)检验运行时开销与产物大小。
关于产物体积,results.json中记录的两条真实数据可作为参考:
41_size-uncompressed:49.5 KB42_size-compressed:14.4 KB
这组数据与构建管线直接相关:esbuild 以--bundle --minify将src/main.ts与@domphy/core合并为单文件dist/main.js,压缩后仅 14.4 KB(包含完整运行时与基准应用),侧面印证了 Domphy 作为轻量级 UI 运行时的体量。
八、实现要点小结:为什么这套写法是"idiomatic"
回到 README 开头的四句话,再对照源码,可以总结出这套 keyed 实现的设计闭环:
- 状态粒度决定重渲染粒度:列表级
data只处理增删排,文本级label只处理内容更新,selected只处理选中样式——三者互不干扰; - 描述符稳定性是性能前提:
WeakMap缓存的元素描述符让 keyed 协调器在重排时拥有稳定的引用,避免重建与全量 patch; - 读取表达式即依赖声明:
(l) => x.get(l)同时完成"读值"与"订阅"两件事,这正是 Domphy"listener-based reactivity"的编程模型; - 标签对象字面量即 UI:无需 JSX 编译器与虚拟 DOM diff,从 src/main.ts 的
App对象到真实 DOM,中间只隔一层ElementNode。
如果要在本地复现并验证本实现,按 README.md 中的说明执行即可:
cd frameworks/keyed/domphy npm install npm run build-prod随后通过 index.html 加载dist/main.js,即可在浏览器中体验 Create 1,000 rows、Update every 10th row、Swap Rows 等全部基准交互。需要说明的是,dist/main.js为构建产物,仓库并未提交该文件,构建前的目录中仅有src/main.ts作为源码入口。
- 性能测试
- 开发者工具
【免费下载链接】js-framework-benchmark
A comparison of the performance of a few popular javascript frameworks
相关推荐
大众点评数据采集终极解决方案:深度破解动态字体加密,高效获取全站商家信息
大众点评数据采集终极解决方案:深度破解动态字体加密,高效获取全站商家信息 探索大众点评数据采集的完整技术方案,揭秘如何绕过复杂的反爬机制,实现稳定高效的全站数据
性能测试开发者工具CANN/asc-devkit SIMT sinpif函数
sinpif 产品支持情况 <! npu="950" id1 Ascend 950PR/Ascend 950DT:支持 <! end id1 <! npu="A
人工智能深度学习算子库CANNAscend基于 Inferno JSX Flags 的应用级运行时优化与 Rollup 构建实践(js-framework-benchmark 实例解析)
基于 Inferno JSX Flags 的应用级运行时优化与 Rollup 构建实践(js framework benchmark 实例解析) 在 js fr
性能测试开发者工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考