Astro поддерживает View Transitions как встроенный API. Достаточно задать viewTransitionName на двух элементах в разных маршрутах — и браузер автоматически анимирует переход между ними без JavaScript-библиотек.
Но деплой на Cloudflare Workers со Static Assets выявил несколько подводных камней, которые стоит зафиксировать.
Конфигурация
Портфолио работает на Astro 6 с output: "static". Каждая страница пререндерится при сборке и раздаётся с edge-серверов Cloudflare как обычный HTML-файл. Единственный динамический маршрут — /api/brief, обрабатываемый Worker’ом.
View Transitions живут целиком в браузере — сервер о них ничего не знает. Это хорошая часть.
Как работает морф
Два элемента разделяют общий view-transition-name:
<!-- PostCard.astro (страница индекса) -->
<h3 style={{ viewTransitionName: `thought-${slug}-title` }}>
{title}
</h3>
<!-- [slug].astro (страница поста) -->
<h1 style={{ viewTransitionName: `thought-${slug}-title` }}>
{title}
</h1>
При навигации между ними браузер захватывает снимки обоих элементов и плавно морфит один в другой. Без Motion, без FLIP — чистый CSS под капотом.
Подводные камни
Три проблемы проявились при деплое:
- Дублирование имён переходов — если две карточки на индексе имеют одинаковое имя, браузер молча отбрасывает переход. Каждое имя должно быть уникальным на странице.
- MPA vs SPA режим — Astro по умолчанию использует MPA-переходы (полная загрузка страницы). Компонент
<ViewTransitions />переключает на перехват навигации в стиле SPA. - Кеширование Cloudflare — Workers агрессивно кешируют. Устаревший HTML-ответ может ссылаться на старое имя перехода, которого больше нет на целевой странице.
Решение проблемы кеширования
Мы добавили Cache-Control: no-cache для HTML-ответов, сохранив долгосрочное кеширование для статических ресурсов (JS, CSS, изображения):
// Worker fetch handler
export default {
async fetch(request: Request, env: Env) {
const response = await env.ASSETS.fetch(request);
const url = new URL(request.url);
if (url.pathname.endsWith("/") || url.pathname.endsWith(".html")) {
const headers = new Headers(response.headers);
headers.set("Cache-Control", "no-cache");
return new Response(response.body, { ...response, headers });
}
return response;
},
};
Это не специфично для View Transitions — это хорошая практика для любого статического сайта, где HTML меняется при каждом деплое, а ресурсы хешируются.
Замечания по производительности
View Transitions добавляют ноль килобайт JavaScript, если использовать встроенную поддержку Astro. Браузерный API document.startViewTransition() делает всё сам.
Мы измерили через Lighthouse на типичной странице поста:
| Метрика | До | После |
|---|---|---|
| Performance | 98 | 98 |
| FCP | 0.8s | 0.8s |
| LCP | 1.1s | 1.1s |
| CLS | 0 | 0 |
Регрессии нет. Сам переход добавляет ощутимую задержку ~250ms (длительность морфа), но пользователи воспринимают её как намеренную анимацию, а не как время загрузки.
Reduced motion
Переходы уважают prefers-reduced-motion. Когда пользователь включил уменьшение движения, Astro переключается на мгновенное растворение — всё ещё лучше, чем резкая смена страницы, но без плавных морфов.
Полная реализация живёт в шаблоне поста и компоненте PostCard. Паттерн обобщается на любую пару маршрутов с общим контентом — кейсы используют тот же подход.