웹 컴포넌트가 성능과 개발자 경험에서 부족한 이유
Lit이나 Symbiote 같은 웹 컴포넌트는 각 요소를 HTMLElement로 생성하여 C++ 힙에 약 124바이트를 할당합니다. 이는 $mol의 JS 객체 16바이트보다 8배 더 많은 양입니다. 1,000개의 작업에 대해 Lit/Symbiote는 DOM 노드만으로 약 124KB를 소비하는 반면, $mol은 약 16KB를 사용합니다. 10,000개의 작업에서는 이 차이가 1.2MB 대 160KB로 커집니다.
가상화 없이 수천 개의 요소가 있는 인터페이스는 지연되기 시작합니다. 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의 저자는 확인합니다: 최대 성능을 위해 웹 컴포넌트는 적합하지 않으며, DOM 노드를 최소화하는 것은 불가능합니다.
DOM 작업으로 인한 성능 손실
DOM 작업은 C++ 바인딩과 JIT 최적화 부재로 인해 JS 속성보다 수십 배 느립니다:
| 작업 | 시간 | 얼마나 더 나쁜가? |
|----------------------------|-------------|-------------------|
| obj.title = x (JS) | 1–2 ns | 1x |
| element.textContent = x | 30–60 ns | 30x |
| element.setAttribute | 50–100 ns | 50x |
| element.style.color = x | 80–150 ns | 80x |
대량 업데이트 중(예: TodoMVC에서 모든 작업 선택) 성능이 저하됩니다:
| 작업 수 | Lit | $mol | 차이 |
|-----------|------------|----------|-------------|
| 100 | 60 µs | 3 µs | 20x |
| 1,000 | 600 µs | 8 µs | 75x |
| 10,000 | 8 ms | 12 µs | 650x |
| 100,000 | 120 ms | 15 µs | 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 파싱은 오버헤드를 추가합니다.
반응성의 차이
웹 컴포넌트는 푸시 의미론을 사용합니다: 변경 시 이벤트가 모든 구독자에게 브로드캐스트됩니다.
// 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)은 불필요한 업데이트를 최소화하고 코드 양을 줄입니다. 푸시는 구독 오류 위험을 증가시킵니다.
테스트와 상속
웹 컴포넌트 테스트는 DOM이 필요합니다: 요소 생성, connectedCallback, updateComplete 대기. $mol에서는 DOM 없이 JS 객체로 테스트가 작동합니다.
// $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
// 나머지는 모두 상속됩니다
핵심 요약
- 웹 컴포넌트는 요소당 약 124바이트를 DOM에 소비하며, 이는 목록에 치명적입니다.
- DOM 작업은 JS 속성보다 30–80배 느리며, 규모가 커질수록 성능이 저하됩니다.
- 푸시 반응성은 수동 구독이 필요하며, 풀은 더 간단하고 효율적입니다.
- 웹 컴포넌트 테스트는 DOM으로 인해 느립니다; $mol은 순수 객체를 테스트합니다.
- 웹 컴포넌트의 상속은 템플릿 복사-붙여넣기를 초래합니다.
— Editorial Team
아직 댓글이 없습니다.