Почему этот блог отдаёт почти ноль JavaScript
Сайт, который ты сейчас читаешь, собран на Astro. Рассказываю про архитектуру островов и где я беру Astro, а где всё-таки Nuxt.
Сайт, который ты открыл, собран на Astro. Выбор осознанный, и он неплохо показывает мысль, которую я повторяю на работе: инструмент подбирается под тип страницы, а не под резюме команды.
От чего отталкивался
Возьми обычный блог на любом фреймворке. Страница статьи - это текст, картинки и три интерактивных элемента: меню, фильтр по тегам и плеер. А браузер получает целый фреймворк, гидратирует всё дерево и тратит процессорное время на оживление абзацев, которые оживлять незачем.
Astro переворачивает подход: по умолчанию на клиент уходит ноль JavaScript. Компоненты выполняются при сборке и превращаются в HTML. Интерактивность добавляется точечно, островами.
---
// это выполняется при сборке, в браузер не попадает
const posts = await getCollection('blog');
---
<article>
<h1>Заголовок</h1> <!-- статичный HTML -->
<PostList posts={posts} /> <!-- тоже статичный -->
<PodcastPlayer client:visible /> <!-- остров: JS приедет, когда доскроллят -->
</article>
Директивы client:* - это и есть управление тем, что и когда оживает: client:load сразу, client:idle когда браузер освободится, client:visible при появлении в зоне видимости, client:media по медиазапросу. Каждый остров гидратируется отдельно, и если один упал, остальная страница продолжает работать. В монолитном SPA одна ошибка в компоненте способна снести весь рендер.
Чем это отличается от обычного SSR
SSR тоже отдаёт готовый HTML, но потом гидратирует всю страницу целиком: фреймворк грузится всегда, дерево обходится полностью. Острова оживляют только помеченные куски и только когда надо.
Разница особенно заметна на слабых телефонах - там каждая лишняя сотня килобайт JS превращается в заметную задержку. Самый надёжный способ оптимизации вообще не в оптимизации кода, а в том, чтобы его не отправлять.
Где Astro, а где Nuxt
Я делю так:
Контент, который читают - Astro. Блог, документация, лендинг, портфолио. Интерактивность тут исключение.
Приложение, в котором работают - Nuxt. Админки, кабинеты, дашборды. Интерактивность тут норма, и острова превращаются в архипелаг, которым неудобно управлять.
Признак, что ошибся с выбором: в Astro-проекте больше половины компонентов помечены client:load. Значит, это приложение, а не сайт - бери фреймворк и не мучайся.
Приятный бонус - Astro не привязан к одному фреймворку. Острова можно писать на Vue, React, Svelte в одном проекте. Для постепенных миграций иногда решающий аргумент.
Что ещё нравится
Контент лежит в Markdown, а схема описывается на Zod и проверяется при сборке:
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
Забыл описание в статье - сборка падает с понятной ошибкой. Опечатался в дате - тоже. Тот же принцип, что и с валидацией схемой на фронте: ошибку ловим там, где она дешевле всего стоит.
Плюс можно смешивать статику и сервер: у меня почти всё предгенерировано, а эндпоинт с эпизодами подкаста работает на сервере с часовым кэшем.
Из честных минусов: экосистема меньше, часть готовых решений придётся написать руками. Для блога это ерунда (ну что там писать - фильтр по тегам), для сложного продукта - риск, который надо посчитать заранее.
Вывод у меня не про Astro, а про вопрос, который стоит задавать: сколько интерактивности реально нужно этой странице? Часто ответ - почти никакой, и тогда фреймворк на клиенте это плата ни за что.
Ссылки
- Архитектура островов в Astro
- Директивы
client:* - Content Collections
- Рендеринг по запросу
- Islands Architecture - статья, откуда пошёл термин