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-движка
Внутренности компилятора

0

1

Войти