View Transitions in Astro Without Hurting Vitals
View transitions make a multi-page site feel like an app: the header stays put, the article slides in, an image morphs from a card into the page hero. Astro offers two ways to get them. The <ClientRouter /> component intercepts navigations, fetches the next page and swaps it in with animations, in every browser. Native cross-document view transitions, enabled with one CSS rule, let the browser animate between two ordinary page loads, in browsers that support them. Both can make a site feel faster. Both can also make it measurably slower if transitions are long, scripts misbehave, or the new page has to wait for the animation.
This guide adds view transitions to a 700-page Astro 5 documentation site both ways, measures navigation speed and Core Web Vitals for each, and shows the settings that kept vitals intact. It belongs to Resource Hints and Navigation Speed; generator-neutral background is in View Transitions on Multi-Page Static Sites.
Prerequisites
- Astro 5 with a shared layout.
- Field measurement of vitals with the web-vitals library, so effects show up in real data.
- Analytics that can be told about client-side navigations, if using the ClientRouter.
Option A: Native Cross-Document Transitions
One CSS rule opts in, on both the old and new page:
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 180ms; }
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}
Elements that should morph between pages get matching view-transition-name values — the header, and the docs sidebar so it stays still while the content changes:
.site-header { view-transition-name: header; }
.docs-sidebar { view-transition-name: sidebar; }
Every navigation is still a full page load: the new page's HTML, CSS and scripts load normally, LCP is measured for each page, and analytics see ordinary page views. Browsers without support navigate as before.
Option B: Astro's ClientRouter
---
import { ClientRouter } from 'astro:transitions';
---
<head><ClientRouter /></head>
With the router, Astro fetches the next page, swaps the document and animates. It works in every browser (using a fallback animation where the API is missing), and with prefetch it can make navigations near-instant. The cost is that the page is no longer reloaded, so:
- Scripts — module scripts run once per session. The site's tab component, copy buttons and analytics page-view call all broke on navigated pages until they moved to the
astro:page-loadevent:
document.addEventListener('astro:page-load', () => { initTabs(); initCopyButtons(); });
- Metrics — the web-vitals library reports LCP and CLS for the initial load only; navigations are "soft" and do not produce new LCP entries in standard tooling. Field LCP then describes first visits only.
Step 2: Measure Navigation Speed
Navigation from a docs article to the next, with prefetch on hover enabled in both cases (prefetch: { prefetchAll: false, defaultStrategy: 'hover' }), measured on a mid-range phone profile with 4G throttling, median of 15 navigations:
| Measure | No transitions | Native, 180 ms | ClientRouter, 180 ms | Native, 400 ms |
|---|---|---|---|---|
| Time to new content visible | 640 ms | 690 ms | 420 ms | 900 ms |
| Header stable (no flash) | No | Yes | Yes | Yes |
| JS added | 0 | 0 | 7 KB | 0 |
The ClientRouter was fastest because it reuses the document and its loaded CSS. Native transitions added about 50 ms at 180 ms duration — the browser holds the old page's snapshot until the new page is ready, then animates — and at 400 ms the animation itself became the slowest part.
Step 3: Keep CLS at Zero
View transitions animate snapshots, so they do not cause layout shifts themselves. Shifts appeared in two other ways:
- With the ClientRouter, the docs sidebar was re-rendered on each swap and scrolled back to the top, then a script scrolled it to the active item — a visible jump, recorded as CLS on soft navigations by RUM tools that track them.
transition:persiston the sidebar kept it and its scroll position across navigations. - With both options, images on the new page without dimensions shifted content after the animation ended — the same fix as always, covered in Fixing CLS From Images Without Dimensions in Markdown.
Step 4: Keep INP Low
Clicking a link during a running transition is ignored until it ends, which counts against INP if transitions are long. At 180 ms this was negligible; at 400 ms field INP on docs pages rose from 90 to 140 ms in a one-week trial, because readers who clicked through quickly waited for animations. The site uses 180 ms and no transition at all for in-page anchor links.
Testing Transitions
Two tests guard the setup. A Playwright test navigates between three docs pages by clicking links and asserts that the header element is never detached and that tabs and copy buttons work on the third page — the check that would have caught the ClientRouter script bug in the comparison. A second test runs with reducedMotion: 'reduce' in the browser context and asserts that document.getAnimations() is empty during a navigation, so the reduced-motion rule cannot be broken by a later CSS change. Both take a few seconds and run on every pull request that touches layouts or global CSS.
Named transitions need one more check: view-transition-name values must be unique on a page, or the browser skips the transition entirely. A component that renders a list of cards, each with the same name, produced exactly that failure on the blog index during development. Generating names from the item's slug (view-transition-name: card-${slug}) fixed it, and a post-build script now scans each page's inline styles and CSS for duplicate names.
Measured Impact
The site shipped native transitions at 180 ms. Field data, four weeks before and after:
| Measure | Before | Native transitions | Tool |
|---|---|---|---|
| LCP p75, mobile | 1.7 s | 1.7 s | web-vitals RUM |
| CLS p75, mobile | 0.02 | 0.02 | web-vitals RUM |
| INP p75, mobile | 90 ms | 92 ms | web-vitals RUM |
| Pages per session | 3.1 | 3.4 | Analytics |
| JavaScript added | — | 0 KB | Build output |
Vitals were unchanged; pages per session rose by about 10%, which may reflect smoother navigation or other changes in the period. The ClientRouter would have made navigation faster still, but the script changes, analytics changes and loss of per-page LCP data were not worth it for this site.
Pitfalls & Rollback
- Long transitions. Keep them under about 200 ms; the animation becomes the wait.
- ClientRouter without moving scripts to
astro:page-load. Features silently stop working on navigated pages. - Ignoring reduced motion. Turn animations off for readers who ask.
- Assuming field LCP still covers navigations with the ClientRouter. It covers first loads.
- Rollback: native transitions are one CSS rule; removing
@view-transitionturns them off. The ClientRouter is one component, but its script changes should stay compatible with full page loads.
Conclusion
View transitions in Astro can be added without hurting vitals if they stay short, respect reduced motion and do not disturb scripts or layout. For a content site, native cross-document transitions at 180 ms gave a steadier, smoother navigation with zero JavaScript and no change in field LCP, CLS or INP; the ClientRouter was faster but changed how scripts and metrics work, a trade worth making only for app-like sites.
FAQ
Should I use Astro's ClientRouter or native cross-document view transitions?
For a content site, native cross-document view transitions are simpler and keep every navigation a normal page load, which keeps field metrics and analytics straightforward. They work in Chromium-based browsers and Safari 18.2 and later, and other browsers simply navigate without animation. The ClientRouter adds a client-side router that animates in every browser but changes how scripts run and how metrics are measured.
Do view transitions affect Core Web Vitals?
They can. A transition adds animation time before the new page is interactive and visible, and long or blocking transitions delay paints. With the ClientRouter, navigations become soft navigations that do not produce new LCP measurements in standard field tools, so LCP data shifts to first page loads only. Short transitions and prefetching keep the visible effect positive.
How long should a view transition be?
Short. In the measurements in this guide, transitions of 150 to 200 milliseconds felt smooth without making navigation feel slower, while 400 milliseconds made readers on fast connections wait for the animation. Respect prefers-reduced-motion by removing or shortening the animation.
Do scripts run again after a ClientRouter navigation?
Module scripts run only once per page session with the ClientRouter, because the document is not reloaded. Code that sets up behaviour on page load must listen for the astro page-load event instead, or it will not run on pages reached by navigation. This is the most common bug when adding the ClientRouter.
Related
- Parent: Resource Hints and Navigation Speed — making navigation fast.
- View Transitions on Multi-Page Static Sites — the generator-neutral version.
- Prefetching Links in Astro — the prefetch used with transitions.
- Instant Navigation with Speculation Rules — prerendering the next page.
- Measuring INP on Static Sites with RUM — the field data used here.