Архитектура Jaspr: концептуальные отличия от Flutter Web и механизмы работы DOM
Скомпилированное веб-приложение на Flutter часто встречает пользователя пустым экраном и загрузкой нескольких мегабайт движка CanvasKit или Impeller, прежде чем отрисует первый пиксель. Для сложных интерактивных дашбордов или веб-игр это оправданная цена за pixel-perfect консистентность. Но для контентных сайтов, блогов, e-commerce и классических SaaS-платформ такой подход фатален: нулевое SEO, сложная доступность (accessibility) и избыточное потребление памяти. Jaspr появился не как конкурент Flutter, а как ответ на вопрос: что если писать на Dart, используя его строгую типизацию и компонентный подход, но рендерить результат в нативный HTML и CSS, уважая природу браузера?
Иллюзия холста против реальности узлов
Чтобы понять архитектуру Jaspr, необходимо деконструировать подход Flutter Web. Flutter переносит на веб парадигму мобильной разработки: он запрашивает у браузера пустой холст (элемент <canvas>) и берет на себя абсолютно всю работу. Маршрутизация, расчет макетов (layout), отрисовка текста, обработка скролла и жестов — все это реализуется внутри Dart-кода, скомпилированного в WebAssembly или JavaScript, который затем отдает команды низкоуровневому графическому API (WebGL/WebGPU). Браузер в этой схеме низведен до роли тупого терминала.
Jaspr кардинально меняет вектор. Он не пытается переизобрести браузерный движок рендеринга. Вместо этого Jaspr выступает в роли интеллектуального оркестратора нативного Document Object Model (DOM).
Когда вы пишете компонент на Jaspr, вы не описываете пиксели или абстрактные геометрические примитивы. Вы описываете иерархию HTML-узлов. Это означает, что:
- Текст является текстом. Он выделяется нативно, индексируется поисковыми роботами без дополнительных хаков (вроде семантического дерева, которое Flutter Web строит параллельно с канвасом) и обрабатывается экранными дикторами.
- Layout делегируется браузеру. Вместо того чтобы гонять алгоритмы расчета ограничений (Constraints) внутри Dart, Jaspr генерирует CSS-правила (Flexbox, Grid), позволяя браузерному движку (Blink, WebKit) делать то, для чего он был оптимизирован десятилетиями — рассчитывать расположение элементов на лету.
- Размер бандла минимален. Jaspr не тащит за собой графический движок. Минимальное приложение на Jaspr весит десятки килобайт, так как компилятор
dart2jsэффективно вырезает неиспользуемый код (tree-shaking), оставляя только логику компонентов и тонкую прослойку для работы с DOM.
Анатомия DOM-ориентированного фреймворка
В основе Jaspr лежит концепция Virtual DOM (VDOM), знакомая разработчикам по экосистеме React, но адаптированная под реалии языка Dart.
Прямая манипуляция реальным DOM (вызовы вроде document.createElement или element.appendChild) — крайне ресурсоемкая операция. Реальный DOM содержит тысячи свойств для каждого узла. Если при каждом изменении состояния приложения напрямую перестраивать DOM, производительность упадет до неприемлемых значений.
Jaspr решает эту проблему через промежуточную абстракцию. Когда вызывается метод build компонента (аналог build во Flutter), Jaspr не трогает браузерный DOM. Он создает легковесное дерево Dart-объектов, представляющих желаемое состояние интерфейса.
import 'package:jaspr/jaspr.dart';
class UserProfile extends StatelessComponent {
final String name;
const UserProfile({required this.name, super.key});
@override
Iterable<Component> build(BuildContext context) sync* {
yield div(classes: 'profile-card', [
h1([text('Пользователь: $name')]),
button(onClick: () => print('Clicked!'), [text('Подписаться')])
]);
}
}
В этом примере методы div, h1, button и text — это функции-фабрики, которые возвращают экземпляры компонентов Jaspr, а не реальные HTML-элементы. Использование генератора sync* и yield — специфичная для Jaspr идиома, позволяющая возвращать плоский список дочерних элементов без необходимости оборачивать их в дополнительные массивы, что снижает аллокацию памяти.
Механизм согласования (Reconciliation)
Магия производительности Jaspr кроется в алгоритме согласования. Когда состояние приложения меняется (например, через вызов setState в Stateful-компоненте), фреймворк инициирует процесс обновления:
- Генерация нового VDOM: Метод
buildвызывается снова для затронутого поддерева. Создается новое виртуальное дерево. - Diffing (Сравнение): Jaspr сравнивает новое виртуальное дерево со старым, которое было сохранено в памяти с предыдущего рендера.
- Patching (Применение патчей): Вычисляется минимальный набор точечных изменений (добавить класс, изменить текст, удалить узел), и только эти изменения применяются к реальному браузерному DOM.
Теоретическая сложность алгоритма сравнения двух деревьев общего вида составляет , где — количество узлов. Для интерфейсов с тысячами элементов это означало бы миллиарды операций. Jaspr, как и другие современные VDOM-фреймворки, использует эвристический алгоритм со сложностью , основанный на двух допущениях:
- Элементы разных типов (например,
divизменился наspan) генерируют совершенно разные поддеревья. Jaspr не пытается их сравнивать, а полностью уничтожает старый узел и монтирует новый. - Разработчик может помочь алгоритму, используя свойство
keyдля сохранения идентичности элементов между рендерами, особенно в динамических списках.
Роль ключей (Key) в Jaspr идентична их роли во Flutter. Если у вас есть список элементов, и вы меняете их порядок, без ключей алгоритм diffing'а увидит, что содержимое первого узла изменилось, содержимое второго изменилось, и перерисует их оба. Если же узлам присвоены уникальные ключи, Jaspr поймет, что узлы просто поменялись местами, и выполнит дешевую операцию перемещения DOM-узлов (перевешивание указателей в браузере), а не их полное пересоздание.
Жизненный цикл и привязка к DOM (Mounting)
Процесс превращения виртуального дерева в реальный интерфейс называется монтированием (mounting). Точка входа в клиентское приложение Jaspr — функция runApp, которая принимает корневой компонент и селектор DOM-узла, в который приложение будет вмонтировано (обычно это div с id app в index.html).
Во время монтирования Jaspr проходит по дереву VDOM и через механизм JS Interop (взаимодействие Dart и JavaScript) вызывает нативные API браузера. Здесь возникает важный архитектурный нюанс — управление памятью на границе двух сред.
Dart имеет собственный сборщик мусора (Garbage Collector), а JavaScript-движок браузера — свой. Когда Jaspr навешивает обработчик события на DOM-элемент:
button(onClick: () => _handlePress(), [text('Нажми меня')])
Создается замыкание (closure) в Dart, которое передается в JavaScript как callback для addEventListener. Если компонент удаляется из дерева (unmount), Jaspr обязан корректно снять этот слушатель (removeEventListener). Если этого не сделать, возникнет утечка памяти: DOM-узел будет удерживать ссылку на Dart-функцию, не позволяя сборщику мусора Dart очистить память, выделенную под компонент. Архитектура Jaspr инкапсулирует эту логику: при уничтожении VDOM-узла фреймворк автоматически подчищает все привязанные к нему события и таймеры.
Делегирование Layout и CSS-модель
Для разработчика, пришедшего из Flutter, самым большим концептуальным сдвигом при работе с Jaspr становится отказ от виджетов компоновки. Во Flutter вы строите интерфейс, комбинируя Row, Column, Stack, Expanded и Padding. Движок Flutter проходит по этому дереву, передавая ограничения (Constraints) сверху вниз и получая размеры (Sizes) снизу вверх.
В Jaspr этих виджетов нет (хотя можно написать их эмуляторы). Архитектура Jaspr предполагает, что вы используете CSS для управления макетом.
Вместо Column вы используете div с CSS-свойством display: flex и flex-direction: column. Вместо Padding вы не оборачиваете компонент в отдельный виджет, а просто добавляете CSS-класс или инлайновый стиль padding к существующему элементу.
Это не просто синтаксическая разница, это фундаментальное различие в производительности. Браузерные движки (Blink в Chrome, Gecko в Firefox) написаны на C++ или Rust и содержат десятилетия оптимизаций для расчета Flexbox и CSS Grid. Когда Jaspr отдает браузеру HTML с классами, он перекладывает тяжелую математику расчета координат на нативный код браузера. Flutter Web же вынужден компилировать свой собственный layout-движок на Dart в Wasm/JS и выполнять эти расчеты внутри виртуальной машины, что всегда будет медленнее нативного исполнения браузером.
Компиляция и среда выполнения (dart2js и dart2wasm)
Архитектура Jaspr тесно связана с возможностями компиляторов Dart. В режиме клиентского рендеринга (SPA) код Jaspr компилируется с помощью dart2js. Этот компилятор известен своим агрессивным анализом потока управления (control flow analysis) и глобальным tree-shaking.
Поскольку Jaspr опирается на нативные HTML-теги, библиотека фреймворка очень тонкая. Компилятор видит весь граф вызовов и удаляет любые функции и классы, которые не используются в приложении. В результате минифицированный JavaScript-бандл приложения на Jaspr может весить 30-50 КБ, что сопоставимо с ванильным JS или легковесными фреймворками вроде Svelte, и недостижимо для Flutter Web, где только базовый рантайм весит больше мегабайта.
Более того, архитектура Jaspr готова к переходу на dart2wasm — компиляцию Dart напрямую в WebAssembly. В отличие от Flutter Web, которому Wasm нужен в первую очередь для ускорения работы собственного движка рендеринга (Impeller) и тяжелых математических расчетов, Jaspr использует Wasm для ускорения бизнес-логики и алгоритма согласования VDOM. Взаимодействие с DOM из Wasm исторически было узким местом (требовались накладные расходы на вызовы через JS-прослойку), но с внедрением стандарта WasmGC (Garbage Collection в WebAssembly) и прямой интеграции с Web API, Jaspr получает возможность манипулировать DOM напрямую из Wasm-модуля с почти нулевой задержкой.
Граница применимости
Понимание архитектуры Jaspr позволяет четко определить его нишу. Jaspr не заменяет Flutter Web там, где требуется сложная нестандартная графика, манипуляция пикселями, работа с 3D или абсолютная идентичность рендеринга на мобильных устройствах и в вебе (например, Figma-подобные редакторы).
Архитектура Jaspr сияет там, где веб должен оставаться вебом. Это контентные платформы, административные панели, витрины магазинов и любые приложения, где критичны скорость первоначальной загрузки, SEO-индексация, возможность пользователя скопировать текст, работа автопереводчиков в браузере и нативная интеграция с экосистемой веб-аналитики. Jaspr берет строгую типизацию, null safety и компонентную архитектуру Dart, но вместо того, чтобы бороться с браузером, он вступает с ним в симбиоз, выступая умным контроллером для нативного DOM.