Svelte
Внутренности компилятора
Senior-разработчик профилирует медленный компонент и видит в стеке вызовов незнакомые функции вроде template_effect и set. Документация говорит 'Svelte компилируется в эффективный JS', но что именно происходит, остаётся чёрным ящиком. Открыв вкладку JS Output в svelte.dev и посмотрев сгенерированный код, он понимает: разметка превратилась в инструкции, которые создают узлы один раз и точечно обновляют их через эффекты на сигналах. Чёрный ящик становится читаемым кодом, и оптимизировать его теперь можно осознанно.
- На svelte.dev есть REPL с вкладкой JS Output, где видно, во что компилируется любой компонент - это официальный способ изучать внутренности
- Svelte 5 переписан на модель сигналов: компилятор генерирует код, который создаёт сигналы и эффекты, а не сравнивает виртуальные деревья
- Тезис 'у Svelte нет рантайма' маркетингово упрощён: небольшой рантайм есть (планировщик, сигналы), просто в бандл едет не diff-движок
- Понимание вывода компилятора помогает читать стеки в DevTools, где видны внутренние функции вроде template_effect, child, set
- Компиляторный подход - причина, по которой размер базового рантайма Svelte мал и масштабируется лучше при росте приложения
Предварительные знания
- Руны Svelte 5 на уровне применения: `$state`, `$derived`, `$effect` и их роль в реактивности
- Базовое понимание, что .svelte это не валидный JS и должен быть преобразован перед запуском
- Идея сигнала: значение, которое уведомляет зависимые вычисления при изменении
От .svelte к JavaScript-модулю
Файл .svelte не валиден ни как HTML, ни как JS: в нём смешаны блок script, разметка с фигурными скобками и блок style. Браузер такое не исполнит. Задача компилятора - превратить это в обычный JS-модуль, экспортирующий функцию компонента, и в отдельный CSS. Процесс идёт по этапам: разбор в AST, анализ, генерация кода.
На этапе анализа компилятор выясняет, какие значения реактивны и от чего зависит каждая часть разметки. Это та работа, которую рантайм-фреймворк делал бы во время выполнения, сравнивая деревья. Svelte делает её заранее: он уже знает, что текст в одном узле зависит от count, а атрибут другого - от disabled, и генерирует код ровно под эти связи.
Компилятор работает как часть сборки через плагин Vite (@sveltejs/vite-plugin-svelte). Каждый .svelte-файл проходит конвейер при сборке и в dev-режиме на лету. Результат - обычные ES-модули, поэтому tree-shaking, минификация и code splitting сборщика применяются к ним как к любому JS.
Какую работу компилятор Svelte выполняет на этапе сборки, тогда как рантайм-фреймворк делал бы её во время выполнения?
Разметка превращается в шаблон и эффекты
Ключевая идея кодогенерации Svelte 5: разметка распадается на две части. Первая - статический шаблон, который создаётся один раз: структура узлов, не зависящая от состояния. Вторая - набор эффектов, каждый из которых отвечает за одно динамическое место и перезапускается только при изменении своих сигналов. Нет перерисовки всего компонента, есть точечные обновления.
Структура button создаётся ровно один раз из шаблона. Динамическая часть - текст счётчика - обёрнута в template_effect. Этот эффект читает сигнал count через get и, когда count меняется, обновляет только текстовый узел через set_text. Остальная разметка не трогается. Это и есть гранулярность: единица обновления - не компонент, а конкретный узел.
Реальный вывод компилятора подробнее и содержит больше служебных вызовов, но структура та же: создать шаблон, привязать эффекты к динамическим местам, читать значения через get, писать через set. Имена функций (template_effect, child, set_text) можно увидеть в стеках DevTools, и теперь они перестают быть загадкой.
На что распадается разметка компонента при компиляции в Svelte 5?
Руны как синтаксис, а не функции рантайма
Руна выглядит как вызов функции: `$state(0)`, `$derived(a + b)`. Но это не обычная функция, которую можно сохранить в переменную или передать аргументом. Компилятор распознаёт руны как синтаксические маркеры и переписывает их в код на сигналах. Именно поэтому руну нельзя импортировать, переименовать или вызвать динамически - её не существует во время выполнения в том виде, в каком она написана.
Так становится понятно правило из ранних уроков: руны можно использовать только на верхнем уровне script или в .svelte.ts. Компилятор должен видеть руну синтаксически на месте объявления, чтобы переписать её. Спрятать `$state` внутрь обычной функции или условия нельзя - там компилятор её не ожидает и не преобразует, а во время выполнения такого вызова не существует.
Поскольку руны - это синтаксис, обычные правила JS к ним неприменимы. Нельзя написать const s = `$state`; и потом s(0). Нельзя обернуть руну в try или передать в map. Это не ограничение API, а следствие того, что руна исчезает на этапе компиляции, превращаясь в вызовы внутренних функций сигналов.
Почему `$state` нельзя сохранить в переменную и вызвать позже, как обычную функцию?
Почему 'нет рантайма' верно лишь отчасти
Распространённый тезис: 'у Svelte нет рантайма, всё компилируется'. Это удобное упрощение, но буквально оно неверно. Рантайм есть: импорты из svelte/internal/client в сгенерированном коде это и есть рантайм - реализации state, derived, эффектов и планировщика, который решает, когда запускать обновления. Без них сгенерированный код не работает.
Точная формулировка такая: в бандл не едет интерпретатор шаблонов и diff-движок virtual DOM. Рантайм Svelte мал и состоит из примитивов сигналов и планировщика. При этом он дерево-шейкаемый: компонент, который не использует переходы или хранилища, не тянет соответствующий код. Поэтому базовая стоимость близка к нулю и растёт только от реально используемых возможностей.
- Что в бандл НЕ едет — Интерпретатор шаблонов, diff-алгоритм virtual DOM, общий движок reconciliation. Этой фиксированной базовой стоимости у Svelte нет.
- Что в бандл едет — Маленький рантайм сигналов: реализации state, derived, effect и планировщик обновлений. Дерево-шейкаемый, подтягивается по мере использования.
Чтобы снять чёрный ящик с любого компонента, стоит вставить его в REPL на svelte.dev и открыть вкладку JS Output. Сгенерированный код показывает и шаблон, и эффекты, и импорты рантайма. Это лучший способ понять, почему компонент ведёт себя так, как ведёт, и где в стеке DevTools берутся его внутренние функции.
Как точнее всего описать тезис 'у Svelte нет рантайма'?
Связь с другими темами
Внутренности компилятора - фундамент двух соседних тем. Связи:
- Граф реактивности и будущее — Сгенерированные сигналы и эффекты во время выполнения образуют граф зависимостей - предмет следующего урока
- Производительность — Адресный код вместо diff-движка - корень малого бандла и быстрых обновлений, разобранных в уроке о производительности
Итог
- Компилятор разбирает .svelte в AST (script, разметка, style), анализирует зависимости и генерирует обычный JS-модуль
- Разметка превращается в шаблон, который инстанцируется один раз, и набор эффектов, точечно обновляющих конкретные узлы при изменении сигналов
- Руны это не вызовы рантайм-функций в привычном смысле: компилятор распознаёт их как синтаксис и переписывает в код на сигналах
- 'Нет рантайма' - упрощение: маленький рантайм сигналов и планировщик есть, но diff-движок virtual DOM в бандл не едет
- Вывод компилятора читаем: вкладка JS Output на svelte.dev показывает сгенерированный код и снимает чёрный ящик с поведения компонента
Связанные уроки
- sv-44-reactivity-graph-future — Этот урок про то, во что компилируется .svelte; следующий - про граф реактивности, который связывает сгенерированные сигналы во время выполнения
- sv-38-svelte-performance — Производительность Svelte становится понятной, когда видно, какой адресный код генерирует компилятор вместо diff-движка