홈으로 돌아가기

Web Components 문제: 메모리와 속도

이 기사는 Web Components의 주요 단점 분석: DOM 노드 때문의 높은 메모리 소비, JS 객체 대비 느린 작업, push-반응성 문제. $mol과의 비교에서 100k 요소 시 8000배 차이.

Web Components: 요소당 124 bytes 및 리스트 지연
Advertisement 728x90

웹 컴포넌트가 성능과 개발자 경험에서 부족한 이유

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 노드를 최소화하는 것은 불가능합니다.

Google AdInline article slot

DOM 작업으로 인한 성능 손실

DOM 작업은 C++ 바인딩과 JIT 최적화 부재로 인해 JS 속성보다 수십 배 느립니다:

| 작업 | 시간 | 얼마나 더 나쁜가? |

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

Google AdInline article slot

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

| element.textContent = x | 30–60 ns | 30x |

| element.setAttribute | 50–100 ns | 50x |

Google AdInline article slot

| 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

Advertisement 728x90

다음 읽기