Web Components 在性能与开发体验上的短板
像 Lit 和 Symbiote 这样的 Web Components 将每个元素创建为 HTMLElement,导致在 C++ 堆上分配约 124 字节。这比 $mol 中 JS 对象的 16 字节多出 8 倍。对于 1,000 个任务,Lit/Symbiote 仅 DOM 节点就消耗约 124 KB,而 $mol 使用约 16 KB。当任务数达到 10,000 时,差异扩大到 1.2 MB 对 160 KB。
没有虚拟化,包含数千个元素的界面会开始卡顿。Custom Elements 规范要求继承自 HTMLElement,这不可避免地会为不可见元素创建 DOM 节点。
// Lit:每个 todo-item 都是一个 HTMLElement(C++ 堆,至少约 124 字节)
@customElement("todo-item")
export class TodoItem extends LitElement { ... }
// <todo-item> 在 createElement 时立即分配 DOM 节点
// Symbiote:类似地,每个 <todo-item> = HTMLElement
class TodoItem extends Symbiote { ... }
TodoItem.reg('todo-item');
// $mol:组件是一个 JS 对象。DOM 仅在渲染时创建。
// 模型中的 1,000 个任务 ≠ 1,000 个 DOM 元素
// $mol 只渲染可见组件(虚拟化)
task_rows() {
return this.task_ids_filtered().map(id => this.Task_row(id))
}
SolidJS 的作者确认:为了获得最佳性能,Web Components 并不适用,因为无法最小化 DOM 节点。
由于 DOM 操作导致的性能损失
由于 C++ 绑定和缺乏 JIT 优化,DOM 操作比 JS 属性慢许多倍:
| 操作 | 时间 | 慢多少倍 |
|----------------------------|------------|----------------|
| obj.title = x (JS) | 1–2 纳秒 | 1x |
| element.textContent = x | 30–60 纳秒 | 30x |
| element.setAttribute | 50–100 纳秒| 50x |
| element.style.color = x | 80–150 纳秒| 80x |
在大规模更新期间(例如在 TodoMVC 中选择所有任务),性能会下降:
| 任务数 | Lit | $mol | 差异倍数 |
|----------|-----------|---------|------------|
| 100 | 60 微秒 | 3 微秒 | 20x |
| 1,000 | 600 微秒 | 8 微秒 | 75x |
| 10,000 | 8 毫秒 | 12 微秒 | 650x |
| 100,000 | 120 毫秒 | 15 微秒 | 8000x |
// Lit:通过 DOM 属性更新
render() {
return html`<label> ${this.text} </label>`;
// Lit 底层执行:node.textContent = value
}
// $mol:通过 JS 属性更新
// this.title() — 从记忆缓存中读取(JS 堆)
// DOM 通过 requestAnimationFrame 批量更新
task_title(id, next?) {
return this.task(id, ...)!.title ?? ''
}
Lit 中的 HTML 解析增加了开销。
响应式系统的差异
Web Components 使用推送语义:任何更改时,事件都会广播给所有订阅者。
// Lit:通过 EventTarget + requestUpdate 推送
export class Todos extends EventTarget {
#notifyChange() {
this.dispatchEvent(new Event("change")); // 推送!
}
add(text) {
this.#todos.push({ ... });
this.#notifyChange(); // ← 手动推送
}
}
// 每个组件通过 @updateOnEvent("change") 订阅
// 任何更改时,所有订阅者都会收到通知
// $mol:拉取 — 自动依赖图
@$mol_mem
groups_completed() {
// 自动跟踪依赖:
// task_ids() → task() → groups
// 仅在依赖项更改时重新计算
for (let id of this.task_ids()) {
var task = this.task(id)!;
groups[String(task.completed)].push(id);
}
return groups;
}
// 无需 EventTarget 或手动订阅
拉取响应式($mol、Vue)最小化了不必要的更新并减少了代码量。推送增加了订阅错误的风险。
测试与继承
测试 Web Components 需要 DOM:创建元素、connectedCallback、等待 updateComplete。在 $mol 中,测试使用 JS 对象而无需 DOM。
// $mol:无需 DOM 的测试
'task add'($) {
const app = $hyoo_todomvc.make({ $ })
app.Add().value('test')
app.Add().submit()
$mol_assert_equal(app.task_rows().at(-1)!.title(), 'test')
}
在 Lit 中继承需要复制整个 render() 块:
class MyTodoApp extends TodoApp {
render() {
// 复制 40+ 行 HTML,仅更改页脚
}
}
在 $mol 中,继承仅替换必要的子元素:
$my_custom_todo $hyoo_todomvc
foot <= My_footer $mol_view
// 其他所有内容都继承
关键要点
- Web Components 每个元素在 DOM 上花费约 124 字节,这对列表至关重要。
- DOM 操作比 JS 属性慢 30–80 倍,在规模扩大时性能下降。
- 推送响应式需要手动订阅;拉取更简单高效。
- Web Components 测试因 DOM 而缓慢;$mol 测试纯对象。
- Web Components 中的继承导致模板复制粘贴。
— Editorial Team
暂无评论。