Svelte
Тонкая реактивность: как работают сигналы
Таблица из тысячи строк, и пользователь меняет одну ячейку. В модели с виртуальным DOM фреймворк заново вызывает функцию компонента, строит новое дерево из тысячи строк и сравнивает его со старым, чтобы найти ту единственную изменившуюся ячейку. Svelte 5 поступает иначе: при компиляции он уже знает, что эта ячейка зависит от этого значения, и при изменении обновляет ровно один узел DOM, не трогая остальные 999 строк. Эта разница и есть тонкая, сигнальная реактивность.
- Редакторы вроде интерфейсов с живым превью: правка одного поля обновляет один участок, а не пересобирает весь экран
- Финансовые дашборды: котировки тикают десятки раз в секунду, и точечные обновления узлов экономят работу браузера
- Большие формы: ввод в одно поле не запускает перерасчёт валидации всех остальных полей
- Списки и таблицы: добавление строки не вызывает повторного прохода по всему уже отрисованному списку
Предварительные знания
- Знание базовых рун: `$state` для состояния и `$derived` для производных значений
- Понимание того, что такое реактивность: значение меняется, и интерфейс обновляется
- Базовое представление о DOM как о дереве узлов, которые можно менять точечно
Сигнал: значение, которое знает своих читателей
Сигнал это ячейка реактивного значения с двумя свойствами: её можно прочитать и в неё можно записать. В отличие от обычной переменной, сигнал ведёт учёт того, кто его читал. Когда значение меняется, сигнал оповещает всех своих читателей, что им пора пересчитаться. В Svelte 5 руна `$state` создаёт такой сигнал-источник, а компилятор превращает обращения к переменной в вызовы чтения и записи под капотом.
На уровне исходника count выглядит как обычная переменная. После компиляции это сигнал. Чтение count в разметке регистрирует текстовый узел кнопки как читателя. Запись count++ помечает сигнал изменённым и планирует обновление только этого читателя. Компилятор делает работу заранее, поэтому в рантайме не нужно искать, что поменялось: связи уже установлены.
- Обычная переменная — Хранит значение и ничего не знает о том, кто его использует. Изменение никого не оповещает, обновлять интерфейс нужно вручную
- Сигнал — Хранит значение и список читателей. Изменение оповещает читателей, и они пересчитываются сами. Это и есть реактивность на уровне значения
Сигналы это не изобретение Svelte. Похожая модель лежит в основе SolidJS, Vue (через refs и computed) и Angular signals. Svelte 5 отличается тем, что прячет сигналы за рунами и переносит максимум работы на компилятор, а не на рантайм-библиотеку.
Чем сигнал принципиально отличается от обычной переменной JavaScript?
Отслеживание зависимостей и точечные обновления
Связи между сигналами не объявляются вручную, а собираются автоматически. Когда выполняется производное вычисление или эффект, среда исполнения отмечает, какие сигналы были прочитаны во время этого выполнения. Прочитанные сигналы становятся его зависимостями. Если позже любой из них изменится, это вычисление будет помечено для пересчёта. Это называется автоматическим отслеживанием зависимостей.
При первом вычислении total среда видит чтение price и qty, и оба становятся его зависимостями. Изменение price помечает total устаревшим, и текстовый узел с total обновится. Изменение постороннего сигнала, который total не читает, его не затронет. Так пересчитывается ровно то, что зависит от изменившегося значения, и ничего сверх этого.
Поскольку зависимости собираются при чтении, условная логика учитывается. Если производное читает сигнал b только в одной ветке if, то в проходах, где эта ветка не выполнялась, b не считается зависимостью. Набор зависимостей пересобирается на каждом вычислении, а не фиксируется один раз.
Производное total = `$derived(price * qty)` читает только price и qty. В компоненте есть ещё сигнал discount, который total не использует. discount меняется. Что произойдёт с total?
Сравнение с моделью ре-рендера React
React использует другую модель. При изменении состояния React заново вызывает функцию компонента, получает новое описание UI (виртуальное дерево) и сравнивает его с предыдущим, чтобы найти разницу и применить её к DOM. Гранулярность здесь это компонент: повторно выполняется вся его функция, даже если изменилось одно поле. Чтобы избежать лишних повторных вызовов у детей, разработчик применяет мемоизацию, а с React Compiler её расставляет компилятор.
Сигнальная модель Svelte 5 работает на уровне отдельного значения, а не компонента. Изменение сигнала пересчитывает только зависящие от него производные и обновляет только связанные с ним узлы DOM. Виртуального дерева нет, сравнивать нечего. Функция компонента в Svelte выполняется один раз при создании, чтобы установить связи, а дальше реагируют сами сигналы, а не повторный вызов всей функции.
| Аспект | React (ре-рендер) | Svelte 5 (сигналы) |
|---|---|---|
| Единица обновления | Компонент целиком | Отдельный сигнал и зависящие узлы |
| Виртуальный DOM | Есть, дерево сравнивается на каждое изменение | Нет, обновление идёт напрямую в нужные узлы |
| Функция компонента | Вызывается заново при каждом обновлении | Выполняется один раз при создании |
| Избегание лишней работы | Мемоизация вручную или через компилятор | Встроено: считается только зависящее |
Ни одна модель не является безусловно лучшей. Ре-рендер с виртуальным DOM даёт простую ментальную модель и зрелую экосистему. Сигнальная модель экономит работу на точечных обновлениях и крупных деревьях. Это разные компромиссы между предсказуемостью, объёмом рантайма и стоимостью обновления.
Из-за того что функция компонента в Svelte выполняется один раз, привычные из React приёмы не переносятся напрямую. Логика, которую в React пишут в теле компонента в расчёте на повторный вызов, в Svelte должна жить в `$derived` или `$effect`, иначе она выполнится единожды и не отреагирует на изменения.
В чём ключевое отличие сигнальной модели Svelte 5 от модели ре-рендера React при изменении одного поля состояния?
Связь с другими темами
Этот урок объясняет движок под рунами. Дальше модель применяется к производным и глобальному состоянию:
- Введение в руны — Руны это API, а сигналы это то, во что они компилируются
- `$derived` — Производное значение это сигнал, который пересчитывается только при изменении его зависимостей
- Глобальное состояние в модулях — Те же сигналы работают вне компонента, в .svelte.ts-модулях
Итог
- Под рунами Svelte 5 лежат сигналы: единицы реактивного значения, которые знают, кто от них зависит
- `$state` создаёт сигнал-источник, `$derived` создаёт производный сигнал, а эффекты и фрагменты DOM подписываются на чтение
- Зависимости отслеживаются автоматически: при чтении сигнала внутри эффекта связь между ними запоминается
- Обновления точечные: меняется один сигнал, и пересчитываются только зависящие от него узлы, без сравнения дерева целиком
- В отличие от ре-рендера компонента целиком, сигнальная модель не строит и не сравнивает виртуальное дерево на каждое изменение
Связанные уроки
- sv-05-runes-intro — Руны это поверхностный API над сигналами. Без знания самих рун обсуждать их движок преждевременно
- sv-07-derived — `$derived` это производный сигнал, и его поведение становится понятнее через модель отслеживания зависимостей
- sv-33-shared-state-modules — Глобальное состояние в .svelte.ts это те же сигналы, вынесенные из компонента, и их поведение опирается на эту модель