Господа, продолжаем разбиратся в тонкостях веб компонент. Сделал тут бенч - сравнениe фреймворков ( $mol/lit/symbiot ) по todomcv. Вроде говорим об одном, а бенч о другом, разве не так ? Ан-нет, что бы разобраться с веб компонентами нужны фреймворки которые ставят их во главу угла, те, кто "сделал на них ставку".
Вот что мне удалось понять:
Первое. Память: 124 байта на веб-компонент, и 16 байт на JS object. Разница на порядок, это много, и без виртуализации интерфейс скорее всего будет лагать
// Lit: каждый todo-item — это HTMLElement (C++ heap, ~124 байт minimum) @customElement("todo-item") export class TodoItem extends LitElement { ... } // <todo-item> сразу аллоцирует DOM-ноду при createElement // Symbiote: аналогично, каждый <todo-item> = HTMLElement class TodoItem extends Symbiote { ... } TodoItem.reg('todo-item'); // $mol: компонент — JS-объект. DOM создаётся ТОЛЬКО при рендере. // 1000 задач в модели ≠ 1000 DOM-элементов // $mol рендерит только видимые компоненты (виртуализация) task_rows() { return this.task_ids_filtered().map(id => this.Task_row(id)) }
Custom Element | js/mol_view | |
1 000 задач | ~124 KB только на ноды | ~16 KB на объекты |
10 000 задач | ~1.2 MB | ~160 KB |
И это только базовые ноды — без текстовых, атрибутов, стилей и event listeners, которые тоже аллоцируются в C++ heap. Спецификация Custom Elements требует class MyEl extends HTMLElement. Нельзя создать CE без DOM-ноды.
Вот тут разбирается этот же довод от автора SolidJs. Далее ответ на довод от автора статьи
This is completely true. If your goal is to build the absolute fastest framework you can, then you want to minimize DOM nodes wherever possible. This means that web components are off the table.
Проще говоря - хотите производительности - не используйте веб компоненты.
Второе, помимо увеличения потребления памяти, мы теряем JIT оптимизацию.
Операция | Время | Во сколько раз |
obj.title = x (JS-объект) | ~1–2 ns | эталон - 1x |
element.textContent = x (DOM) | ~30–60 ns | в 30x раз хуже |
element.setAttribute(‘class’, x) | ~50–100 ns | в 50x |
element.style.color = x | ~80–150 ns | в 80x |
Задача - выделить всё ( актуально для почты например, которая ну никак не может за 1 раз удалить все письма, если они не помещаются на страницу ) Будет деградировать вот так
Задач | 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 (лаг, если кадр занимает больше 16 мс) | 15 µs | 8000x |
И такой вопрос к читателям. Стоит ли ориентироватьс в таком случае на js-framework-benchmark ? Мне кажется что нет. Не стоит рендерить то - что не видно. Там борются за спички, а все знают что экономить на спичках не нужно. Ну и вспоминаем цитату про перф выше.
Ну и пример в коде. Тут мы еще и html парсим... ( это плохо )
// Lit: обновление через DOM property // element.textContent = newValue ← C++ binding, slow path render() { return html`<label> ${this.text} </label>`; // Lit под капотом делает: node.textContent = value } // $mol: обновление через JS property // this.title() — чтение из memo-кеша (JS heap) // DOM обновляется батчем через requestAnimationFrame task_title(id, next?) { return this.task(id, ...)!.title ?? '' }
Идём далее. WC ( Web components ) используют push семантику для реактивности. Pull думаю врядли буду поддерживать. На что это влияет ? На количество строк кода написанных вами. А всем людям свойственно допускать ошибки, не даром есть поговорка: "Лучший код тот - который не написан". Смотрим бенч

Ну и пример кода, для наглядности.
// Lit: Push через EventTarget + requestUpdate export class Todos extends EventTarget { #notifyChange() { this.dispatchEvent(new Event("change")); // push! } add(text) { this.#todos.push({ ... }); this.#notifyChange(); // ← ручной push } } // Каждый компонент подписывается через @updateOnEvent("change") // При ЛЮБОМ изменении ВСЕ подписчики получают уведомление // Symbiote: Push через EventTarget аналогично store.addEventListener('change', () => this.#render()); // $mol: Pull — автоматический граф зависимостей @$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, ни ручные подписки
Не зря VueJs выбрал pull реактивность.
Затронем тестируемость. Что бы протетстить веб компонент - его нужно отрендерить. Максимально неэффективно. Думаю, все мы не людим когда тесты выполняются долго.
Посмотрим пример:
// $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') } // Никакого document.createElement, никакого connectedCallback // Просто вызовы методов и проверка состояния // Lit: для тестирования нужен DOM const el = document.createElement('todo-app'); document.body.appendChild(el); // connectedCallback! await el.updateComplete; // ждём рендер! el.shadowRoot.querySelector('.new-todo')... // доступ через Shadow DOM! // В hyoo_todomvc есть 140 строк тестов. // В Lit и Symbiote реализациях — 0. ( а кода написано даже больше )
Про наследование.
Как это в Lit на "удобных маленьких веб компонентах"
class TodoApp extends LitElement { render() { // Весь шаблон — монолитный блок. 40+ строк HTML. return html` <header>...</header> <section> <ul>${this.todos.map(t => html`...`)}</ul> </section> <footer> <span>...</span> <ul>...</ul> <button>...</button> </footer> `; } } // Хочешь изменить только footer? class MyTodoApp extends TodoApp { render() { // Нельзя сказать "возьми render() родителя, замени footer". // Единственный вариант — скопировать ВСЕ 40 строк // и поменять нужный кусок. return html` <header>...</header> // копия <section>...</section> // копия <footer>МОЙ ФУТЕР</footer> // вот ради этого `; } }
Как это в $mol
// Базовый компонент: прокручиваемая область $mol_scroll sub / ← контент scroll_top 0 ← позиция скролла event_scroll ← обработчик // TodoMVC НАСЛЕДУЕТ $mol_scroll и заменяет только sub ( вложенные элементы ): $hyoo_todomvc $mol_scroll sub / <= Page $mol_list ... // Скролл, позиция, обработчик — всё досталось бесплатно. // Переопределили ТОЛЬКО содержимое. // Можно пойти глубже — унаследовать сам $hyoo_todomvc и заменить, например, только футер: $my_custom_todo $hyoo_todomvc foot <= My_footer $mol_view // Всё остальное (список, ввод, фильтры) — наследуется как есть.
Аналогично с наследованием логики и стилей ( уникально каскадно ) . 0 копипасты и boilerplate
И под конец хотел бы еще поспорить с утверждением. Далее моя вольная цитата из текста @i360u ( первая ссылка в статье )
Любое инженерное решение - это компромисс. Само по себе, наличие “против” не перевешивает все возможные “за”, априори. У каждого такого компромисса - есть конкретная цена. Если эта цена приемлема - это повод всерьез рассмотреть использование той или иной технологии.
В какой то степени с ним можно согласиться. Если бы не одно "НО". У нас у всех стоит задача - разработать максимально удобное, быстрое, красивое приложение, без легаси, чистым кодом, который легко поддерживать и всё в таком духе. Еще раз - задача у всех одна. И надо смотреть кто ёё лучше всего решает в совокупности всего frontend мира. Я знаю одно такое решение.
Это $mol - всё остальное, хуже. Это факт.
То что "вендор лок" "страшно непонятно изучать новое" - это деткие отмазки признать факты. Везде в програмировании надо изучать новое. А вендор лока вообще нет. Думайте. Кирилл. Подписаться.
