返回首页

Web Components 问题:内存和速度

本文分析了 Web Components 的关键缺点:由于 DOM 节点导致的高内存消耗、与 JS 对象相比的操作缓慢、push-响应性问题。与 $mol 的比较显示在 100k 元素时差异达 8000 倍。

Web Components:每个元素 124 字节,在列表中延迟
Advertisement 728x90

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 节点。

Google AdInline article slot

由于 DOM 操作导致的性能损失

由于 C++ 绑定和缺乏 JIT 优化,DOM 操作比 JS 属性慢许多倍:

| 操作 | 时间 | 慢多少倍 |

|----------------------------|------------|----------------|

Google AdInline article slot

| obj.title = x (JS) | 1–2 纳秒 | 1x |

| element.textContent = x | 30–60 纳秒 | 30x |

| element.setAttribute | 50–100 纳秒| 50x |

Google AdInline article slot

| 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

Advertisement 728x90

继续阅读