Svelte

Производительность Svelte

Дашборд аналитики рендерит таблицу на 10 000 строк. Каждый тик WebSocket приходит новый снимок, разработчик кладёт весь массив в `$state`, и Svelte рекурсивно оборачивает в прокси каждый объект и каждое вложенное поле. Прокрутка дёргается, профайлер показывает, что время уходит не на отрисовку, а на создание тысяч реактивных прокси на данные, которые целиком меняются раз в секунду. Замена `$state` на `$state.raw` и ключи по id в each возвращают плавность - реактивность перестаёт работать там, где она не нужна.

  • Svelte стабильно держит верхние строчки бенчмарка js-framework-benchmark по размеру бандла и времени старта среди мейнстрим-фреймворков
  • Компилятор Svelte 5 генерирует код на сигналах без virtual DOM, поэтому рантайм-обвязка минимальна и tree-shaking выкидывает неиспользуемое
  • Дашборды (биржевые терминалы, мониторинг) - типичный случай, где наивный `$state` на больших массивах упирается в стоимость прокси
  • Chrome DevTools Performance и встроенный Lighthouse - стандартные инструменты, которыми меряют реальную производительность Svelte-приложений
  • Виртуализация списков (рендер только видимых строк) - решение проблемы 10 000 DOM-узлов, ортогональное самому Svelte

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

  • Гранулярная реактивность Svelte 5: `$state`, `$derived`, `$effect` и идея сигналов
  • Понимание, что `$state` оборачивает объекты и массивы в реактивные прокси
  • Базовое знание блока each и того, что список можно перерисовывать при изменении данных

Откуда маленький бандл

Большинство фреймворков отправляют в браузер рантайм-движок: код, который во время выполнения сравнивает деревья (virtual DOM) и решает, что обновить. Svelte переносит эту работу на этап сборки. Компилятор читает компонент и генерирует адресные инструкции: при изменении конкретного значения обновить конкретный текстовый узел. В браузер едет результат, а не интерпретатор.

  • Рантайм-фреймворк (virtual DOM) — В бандле едет движок diff-алгоритма. Он одинаков для всех приложений, поэтому базовая стоимость фиксирована и платится всегда, даже за hello world.
  • Компилятор (Svelte) — Каждый компонент превращается в небольшой код прямых обновлений. Нет общего движка diff, базовая стоимость близка к нулю, а tree-shaking выкидывает неиспользуемые фичи.

Компилятор знает, что в DOM меняются ровно два места: текст внутри h1 (при смене name) и текст внутри button (при смене count). Он генерирует код, который при изменении count трогает только текстовый узел кнопки. Нет ни сравнения деревьев, ни перерисовки всего компонента. Это и даёт маленький бандл и быстрые обновления одновременно.

Важная оговорка: маленький бандл - не магия, которая сохраняется при любом стиле кода. Тяжёлая зависимость (большая библиотека дат, чарты, редактор кода) весит столько же, сколько весит, независимо от фреймворка. Преимущество Svelte в базовой стоимости самого фреймворка, а не в обнулении прикладного кода.

Почему базовый бандл Svelte-приложения меньше, чем у фреймворка на virtual DOM?

Не переусердствовать с реактивностью

Реактивность не бесплатна. Каждый `$state` с объектом создаёт прокси, каждый `$effect` подписывается на зависимости и запускается при их изменении. Частая ошибка - тянуться к `$effect` там, где нужно вычисляемое значение. `$derived` ленив и пересчитывается только при чтении, а `$effect` запускается на каждое изменение зависимости, даже если результат никому не нужен прямо сейчас.

ИнструментКогда применятьСтоимость
обычная letЗначение не участвует в разметке и не меняется реактивноНулевая, никакой обвязки
$derivedЗначение вычисляется из другого состоянияЛенивый пересчёт при чтении, дублирования нет
$effectНужен побочный эффект: лог, запрос, работа с не-Svelte APIЗапуск на каждое изменение зависимостей

Использование `$effect` для синхронизации одного состояния с другим почти всегда признак ошибки: появляется дубль источника правды и риск каскадных перезапусков. Для производных значений существует `$derived`, для побочных эффектов с внешним миром - `$effect`.

Значение total нужно вычислять из price и qty и показывать в разметке. Что выбрать?

Ключи в each и `$state.raw` для больших данных

Когда список меняется, Svelte должен сопоставить старые элементы с новыми. Без ключа он сопоставляет по позиции: если элемент вставлен в начало, все последующие узлы считаются изменёнными и обновляются зря. Ключ (item.id) говорит Svelte, какой DOM-узел соответствует какому элементу данных, и узлы переиспользуются, а не пересоздаются.

Вторая проблема больших списков - стоимость прокси. Обычный `$state` рекурсивно оборачивает каждый объект массива в реактивный прокси, чтобы отслеживать изменения отдельных полей. Для 10 000 строк, которые приходят с сервера целым снимком и не мутируются по полям, это лишняя работа. `$state.raw` делает реактивной саму ссылку (замена массива целиком триггерит обновление), но не оборачивает содержимое в прокси.

Граница простая. Данные мутируются точечно (пользователь редактирует поле строки) - нужен обычный `$state`, чтобы отследить изменение поля. Данные приходят снимком и заменяются целиком - подходит `$state.raw`: реактивность на уровне ссылки, без затрат на тысячи прокси. Для 10 000 DOM-узлов дополнительно применяют виртуализацию, рендеря только видимые строки.

С сервера каждую секунду приходит снимок на 10 000 строк, который целиком заменяет предыдущий и не редактируется по отдельным полям. Какое состояние выбрать?

Измерять, а не угадывать

Оптимизация вслепую тратит время на места, которые ничего не стоят, и пропускает реальное узкое горлышко. Сначала измерение, потом правка. Для размера бандла - вывод сборщика и анализаторы зависимостей. Для рантайма - вкладка Performance в Chrome DevTools: она показывает, где уходит время, в скрипте, в layout или в перерисовке. Lighthouse даёт пользовательские метрики загрузки.

Что меряемИнструментЧто покажет
Размер бандлаВывод vite build, rollup-plugin-visualizerКакие модули весят больше всего
Рантайм-затратыChrome DevTools PerformanceВремя в скриптинге, layout, paint; долгие задачи
Загрузка страницыLighthouse, web-vitalsLCP, INP, CLS - метрики реального пользователя
Регрессии в CIБюджет бандла в сборкеПадение билда при превышении порога размера

Полезное правило: оптимизировать имеет смысл то, что измерено и подтверждено профайлером. Замена `$state` на `$state.raw` оправдана, когда профиль показывает время в создании прокси; на списке из десяти элементов разницы не будет, а код станет сложнее без причины.

С чего разумно начать работу над производительностью медленной страницы?

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

Производительность - прикладной слой поверх модели реактивности. Связи:

  • Внутренности компилятора — Маленький бандл и скорость - прямое следствие того, что компилятор превращает .svelte в адресный код на сигналах
  • Гранулярная реактивность — Стоимость прокси и переусложнённой реактивности понятна только при знании, как устроен сигнальный граф

Итог

  • Малый бандл Svelte - следствие компиляции: разметка превращается в адресные обновления DOM, а не в рантайм с virtual DOM
  • Переусердствование с `$state` и `$effect` стоит дорого: лишние прокси и эффекты создают работу там, где хватило бы обычной переменной или `$derived`
  • Ключ в each (each items as item (item.id)) позволяет Svelte переиспользовать узлы вместо пересоздания при изменении списка
  • $state.raw делает значение реактивным целиком, но не оборачивает содержимое в прокси - выбор для больших данных, которые заменяются, а не мутируются по полям
  • Меряют не на глаз: Chrome DevTools Performance, Lighthouse и сравнение размера бандла до и после, иначе оптимизация идёт вслепую

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

  • sv-43-compiler-internals — Производительность Svelte растёт из компилятора - урок о внутренностях показывает, какой код генерируется и почему он быстрый
  • sv-37-testing-vitest-playwright — Перед оптимизацией нужны тесты как сетка безопасности, чтобы ускорение не сломало поведение
Производительность Svelte

0

1

Войти