Проблема
Переходы между представлениями разных документов анимируют элементы между двумя страницами при навигации в пределах одного источника. Браузер делает снимок старой страницы, переходит на новую и анимирует переход от снимка к новой странице. Если на новой странице не загружены критически важные ресурсы (таблицы стилей, скрипты макета или ключевые элементы DOM), переход будет анимирован в неполном или нестилизованном состоянии. Это приводит к визуальным сбоям, таким как перемещение элементов в неправильные позиции, перекомпоновка контента в середине анимации или мигание резервных шрифтов в веб-шрифтах после завершения перехода.
Решение
Используйте blocking="render" для важных элементов <link> и <script> в <head> новой страницы, а также <link rel="expect">, чтобы заблокировать отрисовку до тех пор, пока не будут обработаны определенные элементы DOM. Это гарантирует, что браузер не начнет анимацию перехода, пока визуальное состояние новой страницы не стабилизируется. Браузер продолжает анализировать HTML в фоновом режиме, откладывается только отрисовка.
Стратегия реализации
- ОБЯЗАТЕЛЬНО. Включите переходы между страницами с помощью CSS-правила
@view-transitionна обеих страницах. - ОБЯЗАТЕЛЬНО. Убедитесь, что критически важные таблицы стилей находятся в
<head>. Таблицы стилей в элементе<head>по умолчанию блокируют отрисовку. Для динамически внедряемых таблиц стилей требуется явное указаниеblocking="render". - РЕКОМЕНДУЕТСЯ использовать
blocking="render"для элементов<script>, которые должны быть выполнены до анимации перехода (например, скрипты, которые применяют тему или влияют на макет). - РЕКОМЕНДУЕТСЯ использовать
<link rel="expect" href="#element-id" blocking="render">, чтобы блокировать отрисовку, пока не будет обработан контент в верхней части страницы. Это относится ко всем типам переходов: перекрестному затуханию на всю страницу (чтобы избежать анимации на пустой странице), морфинг-анимации (чтобы именованные элементы существовали в DOM) и макетам, зависящим от скрипта (чтобы стилизованный контент анализировался). - НЕ блокируйте отрисовку некритичного контента. Блокируйте только ресурсы и элементы, которые влияют на начальную область просмотра. Блокировка слишком большого количества контента задерживает переход и ухудшает воспринимаемую производительность.
Руководство по внедрению
Шаг 1. Включите переходы между представлениями разных документов
ОБЯЗАТЕЛЬНО. Исходная и целевая страницы должны содержать правило @view-transition. Без этого переход между документами не произойдет.
/*
MANDATORY: Include this rule in every page that participates
in cross-document view transitions.
`navigation: auto` enables transitions for standard navigations
(link clicks, form submissions, back/forward).
*/
@view-transition {
navigation: auto;
}
/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
@view-transition {
navigation: none;
}
}
Шаг 2. Блокируйте отрисовку до выполнения критически важных скриптов
Если неблокирующий скрипт в <head> должен выполняться до анимации перехода (например, чтобы применить класс темы или повлиять на макет), пометьте его с помощью blocking="render". В противном случае скрипты async, defer или type="module" могут выполняться после начала перехода.
<head>
<!--
DO: Mark layout-critical scripts with blocking="render".
-->
<script type=module blocking="render">
// Example: apply a stored theme before the page renders,
// so the transition snapshot reflects the correct theme.
document.documentElement.dataset.theme =
localStorage.getItem('theme') || 'light';
</script>
</head>
Шаг 3. Заблокируйте отрисовку до обработки ключевых элементов DOM
Таблицы стилей и скрипты blocking="render" в разделе <head> гарантируют только то, что раздел <head> был полностью обработан. Они не ждут, пока будет обработан контент <body>. Без дополнительной блокировки браузер может сделать снимок новой страницы до того, как элементы в верхней части страницы появятся в DOM. В результате переход будет анимирован на пустую или частично отрисованную страницу.
<link rel="expect"> решает эту проблему, блокируя отрисовку до тех пор, пока не будет обработан определенный элемент (идентифицируемый по его id). Значение href должно быть идентификатором фрагмента (например, #hero), соответствующим атрибуту id целевого элемента. После обработки закрывающего тега элемента блокировка рендеринга снимается. Обратите внимание, что <link rel="expect"> гарантирует только наличие целевого элемента DOM в документе. Он не ждет, пока будут скачаны или декодированы внешние подресурсы, например пиксели <img>.
ИСПОЛЬЗУЙТЕ <link rel="expect"> во всех следующих случаях:
Пример 1. Перекрестное затухание на всю страницу
Даже если ни у одного элемента нет view-transition-name, переход root по умолчанию перекрестно затухает на всей странице. Если снимок новой страницы сделан до того, как был проанализирован контент в верхней части страницы, перекрестное затухание анимируется от старой страницы к пустой или неполной странице. Блокировать отрисовку элемента, который отмечает конец видимого контента в верхней части страницы.
<head>
<link rel="stylesheet" href="/css/styles.css">
<!--
DO: Block rendering until the main content area is parsed,
even for a simple cross-fade. Without this, the browser may
snapshot the page before visible content exists in the DOM,
causing the cross-fade to reveal a blank or partial page.
-->
<link rel="expect" href="#main-content" blocking="render">
</head>
<body>
<header>...</header>
<main id="main-content">
<h1>Page Title</h1>
<p>Above-the-fold content the user should see immediately.</p>
</main>
<!-- Content below the fold does NOT need to be blocked on -->
<section>...</section>
</body>
Пример 2. Анимация морфинга между определенными элементами
Если у элементов на обеих страницах одинаковое значение view-transition-name, браузер плавно преобразует их при переходе. Если целевой элемент не был проанализирован в начале перехода, браузер не сможет его найти, и морфинг превратится в отдельные анимации входа и выхода. Блокировать отрисовку, пока не будет обработан элемент с view-transition-name.
Поскольку <link rel="expect"> приостанавливает отрисовку только до тех пор, пока не будет проанализирован целевой элемент DOM, и не ждет скачивания или декодирования данных изображения, некешированный элемент <img> может быть зафиксирован как пустой прямоугольник на снимке новой страницы. Чтобы избежать пустых снимков или сдвигов макета во время морфинга изображений, предварительно загрузите или декодируйте критически важные изображения перехода (например, с помощью <link rel="preload" as="image"> или предварительной отрисовки целевой страницы) и укажите явные резервные размеры (атрибуты width и height или CSS aspect-ratio, а также цвет фона плейсхолдера).
<head>
<link rel="stylesheet" href="/css/styles.css">
<!--
DO: Preload critical transition images so their pixels are
available to decode before the new-page snapshot is captured.
<link rel="expect"> only waits for the DOM element to be parsed,
NOT for image pixels to download or decode.
-->
<link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">
<!--
DO: Block rendering until the element participating in the
morph animation has been parsed. Without this, the browser
may start the transition before #hero exists, causing the
morph to degrade to a fade-out/fade-in.
-->
<link rel="expect" href="#hero" blocking="render">
<!--
When multiple blocking="render" resources are present,
rendering is blocked until ALL of them are satisfied.
Here, the browser waits for both the script to execute
AND the #hero element to be parsed — whichever comes last.
-->
<script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
<header>...</header>
<section id="hero">
<h1 style="view-transition-name: page-title">Product Name</h1>
<!--
DO: Provide explicit dimensions (width/height or aspect-ratio)
as a sizing fallback so the snapshot captures the correct layout
box even if image decoding is still in progress.
-->
<img
style="view-transition-name: hero-image"
src="/img/product.webp"
width="800"
height="600"
alt="Product"
>
</section>
</body>
Шаг 4. Используйте запросы медиа для адаптивной блокировки отрисовки
В зависимости от размера области просмотра в верхней части страницы может помещаться разное количество контента. Используйте атрибут media в теге <link rel="expect">, чтобы блокировать отрисовку только того контента, который виден при заданной ширине области просмотра.
<head>
<!--
DO: Use media queries to conditionally block rendering.
On wide screens, both the hero and the sidebar are visible,
so block until both are parsed. On narrow screens, only the
hero is visible initially.
-->
<link
rel="expect"
href="#hero"
blocking="render"
media="screen and (width <= 768px)"
>
<link
rel="expect"
href="#sidebar"
blocking="render"
media="screen and (width > 768px)"
>
</head>
Шаг 5. Используйте pagereveal для контекстно-зависимых переходов (необязательно)
Событие pagereveal не требуется для основной стратегии блокировки отрисовки. Оно нужно, только если значения view-transition-name должны назначаться динамически в зависимости от того, откуда перешел пользователь, например при преобразовании определенного элемента списка в заголовок страницы с подробными сведениями.
Если значения view-transition-name заданы статически в CSS или вы используете только перекрестное затухание по умолчанию для всей страницы, пропустите этот шаг.
<head>
<!--
MANDATORY: The pagereveal listener must be registered before
the page renders. Inline small setup scripts with blocking="render"
to avoid an extra network round-trip on cold loads, or use an
external async script with blocking="render" so the listener is
registered early without blocking HTML parsing.
If the listener is registered too late (e.g., in a deferred
script), the event may have already fired.
-->
<script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
if (!event.viewTransition) return;
const from = navigation.activation?.from;
if (!from) return;
const fromUrl = new URL(from.url);
// DO: Assign view-transition-name based on navigation context.
// This enables a morph animation from the product card on the
// list page to the heading on the detail page.
if (fromUrl.pathname === '/products/') {
const heading = document.querySelector('main h1');
if (!heading) return;
heading.style.viewTransitionName = 'product-title';
try {
await event.viewTransition.ready;
} catch (error) {
// Skipped view transitions reject `ready` with InvalidStateError.
if (error.name !== 'InvalidStateError') {
throw error;
}
} finally {
// MANDATORY: Remove the temporary name after the transition
// finishes. Stale names interfere with subsequent navigations
// and prevent the page from entering the bfcache.
await event.viewTransition.finished;
heading.style.viewTransitionName = '';
}
}
});
Рекомендации
- РЕКОМЕНДУЕТСЯ назначать
view-transition-nameс помощью CSS, когда это возможно. Используйте присвоение JavaScript (черезpagereveal) только в тех случаях, когда название зависит от контекста навигации. - РЕКОМЕНДУЕТСЯ использовать небольшие и быстрые скрипты, блокирующие отрисовку. В браузере есть встроенный тайм-аут (около 4 секунд), после которого переход полностью пропускается (отклоняется
viewTransition.readyсInvalidStateError, если переход был инициирован). - НЕ используйте
<link rel="expect">для блокировки элементов, расположенных глубоко на странице и невидимых в исходной области просмотра. Это задерживает переход без визуальной выгоды. - НЕ помечайте атрибутом
blocking="render"загрузчики аналитики, рекламных сетей, менеджеров тегов и других сторонних сервисов, даже если они добавляют контент в исходную область просмотра. Задержка сети для них учитывается в тайм-ауте перехода, а их выходные данные не являются частью стабильного состояния страницы. - НЕ назначайте один и тот же атрибут
view-transition-nameнескольким элементам на одной странице. Если имена повторяются, перенос не выполняется. - Влияние специальных возможностей на время загрузки. Использование
blocking="render"задерживает визуальные обновления и начальную отрисовку. Это предотвращает визуальные сбои для зрячих пользователей, но может вызвать задержку обработки или отложенную инициализацию для программ чтения с экрана и других вспомогательных технологий, которые зависят от визуализированных деревьев специальных возможностей. Взвесьте преимущества визуальной непрерывности и задержки при первоначальном чтении для пользователей, не использующих визуальные средства, и убедитесь, что скрипты, блокирующие рендеринг, минимальны и максимально оптимизированы.
Резервные стратегии
Переходы между представлениями документов – отличный кандидат на прогрессивное улучшение. В браузерах, которые не поддерживают эту функцию, правило @view-transition игнорируется, а стандартная навигация в пределах одного источника выполняется так же, как и без нее. В поддерживаемых браузерах переходы будут плавными, а в остальных – стандартными. Ограниченная поддержка браузеров не является причиной для отказа от внедрения.
Все браузеры, поддерживающие переходы между документами, также поддерживают blocking="render" и <link rel="expect">, поэтому для функций блокировки рендеринга, описанных в этом руководстве, не требуется отдельный резервный вариант.
О чем ещё нужно помнить
- Влияние на производительность. Каждый ресурс, блокирующий рендеринг, задерживает запуск анимации перехода между представлениями. Минимизируйте количество скриптов, блокирующих отрисовку, и используйте
<link rel="expect">только для элементов в верхней части страницы. Предварительно отрисовывайте целевые страницы с помощью Speculation Rules API, чтобы полностью устранить задержки при загрузке. - Поведение при тайм-ауте. Если общее время блокировки рендеринга превышает примерно 4 секунды, браузер пропускает переход (и любой активный промис
viewTransition.readyотклоняется с ошибкойInvalidStateError). Убедитесь, что критически важные ресурсы загружаются в течение этого окна. - Совместимость с bfcache. Временные назначения
view-transition-name, которые не удаляются после перехода, могут помешать помещению страницы в bfcache. Всегда удаляйте динамически назначенные имена в обратном вызовеfinished.