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-load event:
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.
Native cross-document view transitions compared with Astro's ClientRouter Two panels. Native transitions keep every navigation a full page load, measure LCP per page, need no script changes, and animate only in supporting browsers. The ClientRouter swaps pages client-side in every browser, needs scripts moved to the astro page-load event, reports LCP for first loads only, and adds a 7 kilobyte router script. The two approaches compared Native cross-document full page loads LCP per page no script changes Chromium + Safari 18.2+ 0 KB added ClientRouter client-side swaps scripts: astro:page-load LCP on first load only all browsers ~7 KB router
For this content site the native option was chosen; the ClientRouter was measured for comparison.

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:

MeasureNo transitionsNative, 180 msClientRouter, 180 msNative, 400 ms
Time to new content visible640 ms690 ms420 ms900 ms
Header stable (no flash)NoYesYesYes
JS added007 KB0

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.

Milliseconds to new content after clicking a link, by configuration Vertical bars. Without transitions 640 milliseconds. Native transitions at 180 milliseconds duration 690. ClientRouter at 180 milliseconds 420. Native transitions at 400 milliseconds 900. Time until new content is visible 640 ms None 690 ms Native 180 ms 420 ms ClientRouter 180 ms 900 ms Native 400 ms
Moto G Power profile, 4G throttling, hover prefetch on, median of 15 navigations between docs articles.

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:persist on 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:

MeasureBeforeNative transitionsTool
LCP p75, mobile1.7 s1.7 sweb-vitals RUM
CLS p75, mobile0.020.02web-vitals RUM
INP p75, mobile90 ms92 msweb-vitals RUM
Pages per session3.13.4Analytics
JavaScript added—0 KBBuild output
Field LCP, INP and pages per session before and after native view transitions Paired bars. LCP at the 75th percentile stayed at 1.7 seconds. INP moved from 90 to 92 milliseconds. Pages per session rose from 3.1 to 3.4. Field vitals before and after Before After LCP p75 (s) 1.7 1.7 INP p75 90 ms 92 ms Pages per session 3.1 3.4
Four weeks of web-vitals RUM and analytics on each side of the release.

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-transition turns 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.

Filed underHow-tosAstro