В начале мая, Misko Hevery, создатель фреймворка Angular, объявил о своем уходе из Google и команды Angular - в компанию builder.io.
Всего через полтора месяца, на его странице в dev.to, появился Анонс нового фреймворка - Qwik.
Я решил разобраться, что он из себя представляет и зачем нужен.
Qwik сейчас на стадии proof of concept
, доков очень мало и нет соответствующей инфраструктуры, и даже неясно, выстрелит он или нет, но уже понятны основные идеи, и можно потрогать код.
Давайте разберемся, что интересного он нам принесет, и придется ли нам учить новый фреймворк (спойлер: пока рановато).
Зачем нужен Qwik?
Основная задача Qwik, - как можно догадаться из названия, - быть очень быстрым !
Дело в том, что с мая этого года, Google начал учитывать производительность приложения при поисковой выдаче. В частности, оценка считается с помощью набор метрик Web Vitals.
Большинство современных фреймворков могут, с помощью Server Side Rendering, получить быстрые First Contentful Paint и большинство других метрик, однако одна метрика им пока не покоряется - Time to interactive.
Все дело в том, что после отправки HTML сгенерированного на сервере, необходимо также загрузить фреймворк и код приложения, запустить его, и только после этого можно обрабатывать события.
Эту проблему и пытается решить Qwik.
Что такое умеет делать Qwik?
Qwik позволяет написать код который будет переиспользован на сервере и на клиенте, при этом на клиент код будет подгружаться непосредственно перед выполнением.
Работает это вот так:
При первом рендере, сервер сериализует все данные в атрибуты HTML узлов
Затем подгружается маленький (0.5 кб, 1ms запуск) загрузчик Qwik, который подписывается на все события.
Все обработчики событий хранят в себе только текстовую ссылку на файл с обработчиком, никакого кода.
Сам код подгружается только по мере необходимости, например когда пользователь запускает событие.
Если клиенту нужен доступ к состоянию используемому на сервере, его можно получить из DOM дерева.
Что если у меня плохой инет? Придется ли ждать загрузки кода на каждое действие?
На данный момент - да. В будущем это можно будет оптимизировать, - например, подгружать обработчики кликов при наведении мыши, или загружать заранее наиболее используемые обработчики (как guess.js)
Так ли важен Time to interactive, это вообще реальная проблема?
Метрика Time to interactive названа не очень удачно, на самом деле она меряет время последней задачи которая заняла более 50 миллисекунд.
Чтобы понять, насколько она важна, можно глянуть на калькулятор и посмотреть, как именно эта метрика влияет на конечную оценку.
Хотя вес в оценке всего 10%, можно предположить, что остальные метрики будут легко доводиться до максимальных значений с помощью обычного SSR
и, в этом случае, именно Time to interactive может позволить получить преимущество.
Решение такой проблемы, будет наиболее востребовано в интернет магазинах и в коммерческих сайтах, особенно учитывая что у крупнейших игроков рынка результат сейчас в пределах 20-70 из 100.
Свой сайт в можете проверить тут, а Habr.com получил оценку 29.
Про остальные метрики Web Vitals можно прочитать тут.
Почему бы просто не проапгрейдить существующий фреймворк?
На данный момент, архитектура других фреймворков, не позволяет им реагировать на события без инициализации фреймворка и приложения на клиенте.
Возможно в будущем, после добавления соответствующих архитектурных изменений, подобная функциональность появится и у других фреймворков.
Мишко даже выступал на эту тему несколько лет назад по отношению к Angular, но отметил, что он просто размышляет и конкретных планов на имплементацию на тот момент не было.
А нет ли других фреймворков, которые уже решают эту задачу?
Qwik
- не первый фреймворк, который пытается победить Time to Interactive.
Например есть Markojs.com - от Ebay, который пытался решить эту задачу другим способом, но его создатель покинул проект, и сейчас в core team лишь пара человек, которые сфокусированы на поддержке внутри компании. Кстати, один из core разработчиков Marko теперь помогает с Qwik
.
Также есть Astro.js, но они пытаются решить эту проблему не на уровне обработчиков событий, а на уровне кусков приложения (islands)
Что за название такое?
Фреймворк решили назвать Qwik
, по аналогии со словом скорость (quick), как по мне - название не очень удобное:
Плохо гуглится, т.к. Есть несколько компаний с похожим названием
Github адрес занят (monorepo лежит в организации builder.io), та же фигня с npm.
Изначально фреймворк хотели называть qoot, но потом переименовали.
Хватит болтать, покажите мне код!
Разработчики выложили TODO приложение, в котором можно покопаться и оценить часть архитектурных решений, глянуть можно в StackBlitz, (ура, отличная возможность потестить новые Web Containers) или на Github.
Весь код, и клиента и сервера, лежит в одном месте и переиспользуется, давайте посмотрим на компонент заголовка - Header:
import { jsxFactory, QRL, injectMethod } from '../qwik.js';
import { HeaderComponent } from './Header_component.js';
export const _needed_by_JSX_ = jsxFactory; // eslint-disable-line @typescript-eslint/no-unused-vars
export default injectMethod(
HeaderComponent, //
function (this: HeaderComponent) {
return (
<>
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
value={this.$state.text}
on:keyup={QRL`ui:/Header_addTodo#?value=.target.value&code=.code`}
/>
</>
);
}
);
Первое что бросается в глаза - JSX (что забавно видеть от создателя Angular), с одной стороны это хорошо, потому что синтаксис многим знаком, с другой стороны, это может усложнить жизнь разработчика фреймворка. Скорее всего, по мере развития, когда понадобится дополнительная гибкость, им придется писать свой собственный трансформатор. (Так же см. новую статью @nin-jin про шаблоны, где он ругает JSX)
В каждом шаблоне обязательно присутствует такой код:
export const _needed_by_JSX_ = jsxFactory; // eslint-disable-line @typescript-eslint/no-unused-vars
Но он уйдет, когда разработчики начнут использовать новый JSX transform
Также разработчики не забывают про директивы (как в Angular
), например в коде есть такой TODO
// TODO: Create QFor and QIf directive?
<Q for="todos.value" do={(todo) => <Item $item={todo} />} />
<Q if="todos.value.length > 0" then={(value) => <section></section>} />
Обработчики событий
Все обработчики событий лежат в своих собственных файлах, и при добавления событий используется ссылка на этот файл#имя функции:
<input on:keyup={QRL`ui:/Header_addTodo#addTodo?value=.target.value&code=.code`} />
Подробно про формат этой сложнющей штуки можно прочесть тут, а пока давайте разберем что мы видим:
QRL
- Специальный Template Literal Tag, используемый для всех ссылокui:/
- Namespace, может быть задан пользователем, или прийти из либы, наприме qwik:/ для методов фрейворка./Header_addTodo
- путь к файлу где лежит обаботчик#addTodo
- имя функции, которую нудно запустить?value=
- Как и в обычной ссылке можно передать какие-то параметры (query params
).target.value
- значение инпута, относительно Event
Сам код обработчика выглядит так, и использует внедрение зависимостей:
export const addTodo = injectEventHandler(
HeaderComponent,
provideQrlExp<string>('value'),
provideQrlExp<string>('code'),
provideProviderOf(provideEntity(TodoEntity.MOCK_USER)),
async function (
this: HeaderComponent,
inputValue: string,
charCode: string,
todoEntity: () => Promise<TodoEntity>
) {
if (charCode === 'Enter' && inputValue) {
(await todoEntity()).newItem(inputValue);
this.$state.text = '';
markDirty(this);
}
}
);
Внедрение зависимостей
Dependency Injection в Qwik используется повсеместно (привет из Angular)
Есть несколько методов позволяющих внедрить зависимости, например:
InjectFunction
- Помимо основных зависимостей позволяет получить доступ к параметрам переданным по ссылке (?value=.target.value
в примере выше), для этого можно использовать токен provideQrlExp<string>('value')
.
injectMethod
- (как раз в примере выше) то же самое, только дополнительно внедряет контекст функции (this
), это позволяет функциям лежать в отдельных файлах, но при этом вести себя будто они часть класса.
InjectEventListener
- то же самое, плюс доступ к значению Event
события
Подробнее можно посмотреть в коде.
Сущности/Гидрация
Чтобы переиспользовать код, между клиентом и сервером, Qwik использует умную систему сущностей (Enitity): Каждая сущность, уходящая с севера (например TodoListEntity, или TodoItemEntity) привязана к DOM узлу. Её состояние сериализуется и сохраняется в атрибуте этого узла, привязанном к ID сущности.
Это позволяет восстановить на клиенте сущность, которая была сгенерирована на сервере.
Change Detection
Интересно сделано Change Detection: при изменении данных необходимо вызвать функцию markDirty
(прямо как в Angular Ivy), передав туда DOM элемент
, Entity
или компонент
. Функция найдет все затронутые DOM
узлы, добавит им атрибут on:q-render
и запланирует change detection cycle
через requestAnimationFrame
.
Во время change detection cycle
, узлы, которые нужно обновить, будут получены с помощью простого querySelectorAll('[on:q-render]')
Так как все сущности привязаны к DOM, мы можем пометить сущность как измененную и все соотвествующие DOM элементы также обновятся
Взаимодействие между компонентами
Передача значений детям:
Передавать значения, как и в большинстве фреймворков, можно через свойства:
<Item $item={item} />
А получать в обработчике через DI:
export const Item = injectFunction(
provideEntityState<Item>(
provideComponentProp('$item')
),
function ItemTemplate(item: Item) {
// code
}
);
Получение значений от детей
В примере ниже, мы используем функцию emitEvent
из Qwik
, которая позволяет отправить событие open
родительскому компоненту.
Родительский компонент может слушать on:open
и запустить соответствующий обработчик
<my-component on:open="./onOpen"">
<button on:click="base:qwik#emitEvent?$type=open&someArg=someValue">open</button>
</my-component>
С кодом понятно, готово ли это к использованию?
Проект находится в очень ранней стадии, v1 за горами, так что на настоящем проекте использовать это не рекомендуется никому:
Документация минимальная, хотя код неплохо прокоментирован
Поддержки IDE нет, а тут она явно нужна (можно глянуть, как подсветка синтаксиса сломана в примерах, в этой статье)
Не работает в Firefox
Не используется нигде
Откуда я знаю, что завтра Qwik не перестанут поддерживать?
Angular поддерживается Google, React поддерживается Facebook, но даже большая компания не позволяет на 100% предсказать судьбу проекта, достаточно глянуть на flow или killedbygoogle
Qwik поддерживается компанией Builder.io, они основаны в 2017 и у них на гитхабе есть несколько небольших проектов, которые они успешно поддерживают сохраняя количество Issues в разумных пределах, например jsx-lite или builder,
Смогут ли они справиться с большим проектом, если Qwik выстрелит - покажет время.
Где rxjs?
Резонный вопрос, видимо пока применения тут не нашлось.
Где я могу узнать больше?
Вся документация тут, а Мишко обещал целый цикл статей в своем блоге, можно также зайти в Discord проекта, задать вопросы и может быть даже чем-то помочь.
Заключение
В этой статье мы рассмотрели идеи, предложенные разработчиками Qwik.
К API и к некоторым техническим решениям есть много вопросов, да и учить новый фреймворк пока рано, а вот задуматься над идеями и перестроить свою ментальную модель - самое время.
Благодарности
Эта статья была написано публично на моем стриме, там можно глянуть весь процесс, и много интересных деталей, не вошедших в эту статью.
Я также хочу выразить благодарность всем, кто участвовал в написании, подсказывал и задавал вопросы, ну и большое спасибо всем, кто дочитал это до конца!