Svelte

Под капотом: граф реактивности и будущее Svelte

Команда спорит на code review: один утверждает, что `$derived` пересчитывается каждый раз при изменении любой зависимости, другой - что только при чтении. Оба видели разное поведение в логах и не могут договориться. Ответ лежит в том, как устроен граф реактивности Svelte: изменение сигнала помечает зависимые узлы грязными (push), а реальный пересчёт происходит лениво при чтении (pull). Поняв эту гибридную модель, спор закрывается за минуту, а заодно становится ясно, чем Svelte отличается от ре-рендеров React. Это последний шаг курса - от применения рун к пониманию машины под ними.

  • Гибрид push/pull (пометка грязных при записи, ленивый пересчёт при чтении) - современный подход к сигналам, который Svelte 5 разделяет с Solid и сигналами Angular
  • Async Svelte (await прямо в разметке и `$derived`) - заявленное направление развития, меняющее то, как компонент ждёт данные
  • React работает через ре-рендер компонента и сравнение, Vue - через реактивные ссылки и эффекты; Svelte ближе к Vue по идее, но без virtual DOM
  • Сигналы стали общим вектором фронтенда: Angular, Solid, Vue Vapor и Svelte 5 сходятся к похожей модели гранулярной реактивности
  • Понимание графа зависимостей - то, что отличает уверенную отладку реактивности от догадок 'почему этот эффект запустился'

Предварительные знания

  • Гранулярная реактивность Svelte 5 и роли `$state`, `$derived`, `$effect`
  • Понимание из урока о компиляторе, что руны переписываются в примитивы сигналов
  • Базовое представление о ре-рендере в React и реактивности Vue на уровне идеи (желательно, но не строго)

Граф зависимостей сигналов

Реактивность Svelte 5 удобно представлять как направленный граф. Узлы трёх видов. Источник - сигнал состояния (`$state`): значение, которое можно менять напрямую. Производный узел (`$derived`): значение, вычисляемое из других узлов. Эффект (`$effect`): лист графа, который не возвращает значение, а выполняет побочное действие (обновление DOM, лог, запрос). Рёбра - это зависимости: кто из кого читает.

Зависимости в этом графе строятся не вручную и не из списка, а автоматически во время выполнения. Когда вычисляется total, рантайм фиксирует, что в процессе были прочитаны price и qty, и записывает их как зависимости total. Когда выполняется эффект и он читает total, total становится его зависимостью. Так граф собирается сам из реальных чтений, а не из объявлений.

Автоотслеживание объясняет, почему в Svelte 5 не нужен массив зависимостей, как в useEffect React. Зависимости не перечисляются, а наблюдаются: что фактически прочитано при последнем выполнении узла, то и есть его актуальный набор зависимостей. Если условие изменило набор читаемых сигналов, граф перестроится сам.

Как в графе реактивности Svelte 5 определяется, от чего зависит `$derived` или `$effect`?

Push и pull: пометка грязных и ленивый пересчёт

Главный вопрос любой реактивной системы: когда пересчитывать производные значения. Наивный push пересчитывал бы всё зависимое сразу при каждом изменении источника - даже то, что сейчас никто не читает. Наивный pull пересчитывал бы при каждом чтении - даже когда ничего не менялось. Svelte 5 использует гибрид, который берёт лучшее от обоих.

Работает это в два такта. При записи в сигнал (push) рантайм не пересчитывает производные, а только помечает их и зависимые узлы как потенциально устаревшие - 'грязные'. Реальное вычисление откладывается. Когда значение производного действительно читают (pull), рантайм смотрит: если узел грязный, пересчитывает и кэширует результат, если чистый - возвращает кэш. Грязное, но не прочитанное `$derived` не считается вовсе.

ФазаЧто происходитСтоимость
Запись в $state (push)Зависимые узлы помечаются грязными, пересчёта нетДёшево: только пометка по графу
Чтение $derived (pull), узел грязныйПересчёт по формуле, результат кэшируетсяСчитается один раз до следующего изменения
Чтение $derived (pull), узел чистыйВозврат кэша без пересчётаПочти бесплатно
Грязное, но непрочитанное $derivedНе вычисляется вообщеНулевая

Эффекты в этой схеме - особый случай. Они должны выполниться, даже если их результат никто не читает (иначе DOM не обновится). Поэтому планировщик после изменений ставит грязные эффекты в очередь и запускает их батчем в конце такта. Это и есть причина асинхронности обновлений DOM, из-за которой в тестах нужен flushSync: эффекты выполняются не мгновенно при записи, а в запланированный момент.

Гибрид push/pull прямо отвечает на спор из вступления. `$derived` не пересчитывается на каждое изменение зависимости - изменение лишь помечает его грязным. Пересчёт случается лениво при чтении, и только если узел действительно грязный. Поэтому дорогое производное, которое сейчас не отображается, не нагружает приложение.

Что происходит, когда меняется `$state`, от которого зависит `$derived`, но это производное в данный момент нигде не читается?

Направление async Svelte

Граф реактивности изначально синхронный: вычисление узла возвращает значение сразу. Но реальные данные часто асинхронны - запрос к API, чтение файла. Заявленное направление развития, async Svelte, расширяет граф так, чтобы узел мог зависеть от ещё не разрешённого промиса, а разметка - ждать его декларативно, без ручного управления состоянием загрузки в каждом компоненте.

Смысл в том, что граф учится представлять асинхронный узел как полноценную часть зависимостей. Когда меняется id, производное user помечается грязным, запускается новый запрос, а граф координирует, что показать на время ожидания и как обновить разметку по приходу данных. Это убирает ручную обвязку с флагами loading и error, которую сегодня пишут в каждом компоненте вручную.

Async Svelte - активно развивающееся направление, а не финализированный стабильный API на момент этого урока. Конкретный синтаксис и поведение могут отличаться от показанного и меняться между версиями. Важна идея: граф реактивности расширяется в сторону асинхронности, делая ожидание данных частью модели, а не ручной заботой.

Концептуально это продолжение той же линии: переносить рутину с разработчика на машину. Сначала компилятор взял на себя адресные обновления DOM, потом сигналы убрали ручную мемоизацию зависимостей, теперь граф берётся за координацию асинхронных значений. Каждый шаг сокращает количество состояния, которым разработчик управляет руками.

Какую проблему стремится решить направление async Svelte на уровне графа реактивности?

Svelte против рантайм-фреймворков

Собрав картину, можно трезво сравнить подходы. React строится на ре-рендере: при изменении состояния функция компонента вызывается заново целиком, возвращает новое описание UI, и React сравнивает его с прежним, чтобы найти разницу. Единица реактивности - компонент. Ручная мемоизация (или компилятор React) нужна, чтобы не пересчитывать лишнее.

Vue и Svelte идут от сигналов: реактивная единица - не компонент, а отдельное значение. При изменении пересчитывается только то, что от него зависит, без перезапуска всей функции компонента. Разница между Vue и Svelte в том, что Vue применяет изменения через virtual DOM в рантайме, а Svelte компилирует разметку в адресные обновления заранее и virtual DOM не использует вовсе.

  • React: ре-рендер компонента — Состояние меняется - компонент вызывается заново, новое дерево сравнивается со старым (virtual DOM). Единица реактивности - компонент. Мемоизация ручная или через компилятор.
  • Vue: сигналы + virtual DOM — Реактивная единица - значение. Меняется только зависимое, но применение идёт через virtual DOM в рантайме. Без ручной мемоизации в типичном коде.
  • Svelte: сигналы + компиляция — Реактивная единица - значение. Компилятор заранее генерирует адресные обновления узлов, virtual DOM отсутствует. Маленький рантайм сигналов.
АспектReactVueSvelte
Единица реактивностиКомпонентЗначение (сигнал)Значение (сигнал)
Применение измененийVirtual DOM + diffVirtual DOM + diffАдресные обновления, без VDOM
Где работаРантаймРантаймКомпиляция + малый рантайм
Ручная мемоизацияНужна (или компилятор)Обычно не нужнаНе нужна

Вывод курса не в том, что один подход побеждает. Сигналы стали общим вектором: Angular, Solid, Vue Vapor и Svelte 5 сходятся к гранулярной реактивности. Svelte выделяется тем, что доводит идею до компиляции, убирая virtual DOM и держа рантайм маленьким. Это даёт малый бандл и предсказуемые обновления, а цена - чуть менее обширная экосистема и привязка к шагу компиляции. Выбор инструмента - всегда вопрос контекста: размера команды, требований к бандлу, доступного найма и экосистемы.

Зрелое владение Svelte - это не заучивание API рун, а понимание машины под ними: граф зависимостей, push/pull-вычисление, компиляция в сигналы. С этой моделью отладка реактивности, оценка производительности и сравнение фреймворков перестают быть гаданием и становятся рассуждением о конкретном графе.

Связь с другими темами

Финальный урок собирает нити курса: компиляция, реактивность, производительность. Связи:

  • Внутренности компилятора — Граф состоит из сигналов и эффектов, которые генерирует компилятор; это прямое продолжение предыдущего урока
  • Производительность — Лень pull-вычисления и стоимость лишних эффектов из урока о производительности объясняются устройством графа

Итог

  • Реактивность Svelte 5 это граф: сигналы (`$state`) - источники, производные (`$derived`) - промежуточные узлы, эффекты (`$effect`) - листья с побочным действием
  • Модель гибридная: запись помечает зависимые узлы грязными (push), а пересчёт происходит лениво при чтении (pull), поэтому неиспользуемое `$derived` не считается
  • Зависимости отслеживаются автоматически во время выполнения: что прочитано через get при вычислении, то и становится зависимостью узла
  • Async Svelte - направление, где await встраивается прямо в `$derived` и разметку, а граф учится ждать асинхронные узлы
  • Svelte ближе к Vue по идее реактивности, но без virtual DOM и с компиляцией в сигналы; React держится на ре-рендере компонента и сравнении

Связанные уроки

  • sv-43-compiler-internals — Граф реактивности связывает сигналы, которые генерирует компилятор; без понимания кодогенерации граф висит в воздухе
  • sv-38-svelte-performance — Push/pull-вычисление и лень `$derived` напрямую объясняют советы по производительности из соответствующего урока
Под капотом: граф реактивности и будущее Svelte

0

1

Войти

В чём ключевое отличие модели реактивности Svelte от модели React?