Daniel's Blog
9 min read

A Day with Safari and Astro View Transitions: A Complete Debugging Record

astro safari debug view-transitions
Table of Contents

TL;DR

Problem: Astro’s ClientRouter (View Transitions API) causes animation repeat playback and flickering on Safari.

Cause: Safari’s support for View Transitions API is incomplete, this is a known issue.

Solution: Override the document.startViewTransition API on Safari to directly execute the callback without triggering transition animations.

const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userUser);
if (isSafari && document.startViewTransition) {
  document.startViewTransition = (callback) => {
    const result = callback();
    return {
      finished: Promise.resolve(),
      ready: Promise.resolve(),
      updateCallbackDone: Promise.resolve(result),
      skipTransition: () => {}
    };
  };
}

Result: Safari retains SPA navigation (instant page switching) while solving the flickering issue. Other browsers are unaffected.


Preface

I have mild OCD and often feel uncomfortable about small details, including but not limited to this blog transition animation, and various trivial matters in life. You might ask “why nitpick over these small things?” - trust me, I’m also a bit annoyed by this behavior of mine. It makes me struggle and feel uncomfortable for a long time over things too small to matter, even if my clothes get a tiny stain, I’ll feel uncomfortable for several days. I hope I can help you understand WebKit’s drawbacks (or perhaps Chromium’s advantages?) in this article, and view “loading speed” from a UI/UX designer’s perspective.

Why Choose Astro?

My old WordPress blog expired on the dedicated server and I didn’t want to renew it, and couldn’t find suitable VPS/hosting, so I thought I’d try a static blog. Because I’ve seen too many of this type⬇️

“Is your blog the one that adds a bunch of text with gradient-colored code blocks, using serif fonts with huge shadows on semi-transparent backgrounds plus inexplicable canvas effects copied from somewhere, auto-plays NetEase Cloud Music when opened with a Live2D character blocking the player controls, has custom cursors with click effects that automatically adds copyright notices when copying text, and changes the title to inexplicable content when you switch away from the page?”

Hexo blogs, so I had no good impression of Hexo or even static blogs in general, and my philosophy of blogging has always been writing-first. For me, static blogs being more troublesome to deploy and less convenient for writing (this is just my personal opinion - without a UI editor, needing to write Markdown directly is indeed not as convenient for me) is more like what internet early adopters or tech enthusiasts who like tinkering would prefer. But since I don’t have a ready machine, I can’t self-host Ghost (a modern blogging system). So I finally decided to create a static blog with a simpler theme, and Astro claims to be the best-performing content-driven framework.

The Beginning of the Problem

This blog is built with the Astro framework and has ClientRouter (View Transitions API) enabled to achieve smooth transition animations between pages. Everything works perfectly on Chrome - page switching is smooth and animations are fluid.

But when I tested on Safari, problems appeared:

Animations would play twice and come with obvious flickering.

It’s like the first animation wasn’t finished yet, got forcibly interrupted, and then replayed.


Phase 1: Thinking It Was a CSS Animation Issue

Attempt 1: Optimize CSS Transition Properties

Initially I thought it was a CSS animation performance issue. The original code used transition-all:

.animate {
  @apply -translate-y-3 opacity-0;
  @apply transition-all duration-300 ease-out;
}

transition-all monitors changes to all CSS properties, which could cause performance overhead. So I changed it to only monitor needed properties:

.animate {
  transition-property: opacity, transform;
  transition-duration: 300ms;
  transition-timing-function: ease-out;
  will-change: opacity, transform;
  -webkit-font-smoothing: antialiased;
}

Result: No effect. Flickering persisted.

Attempt 2: Various GPU Acceleration Tricks

Next I tried various Safari animation optimization tricks recommended online:

.animate {
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  -webkit-font-smoothing: subpixel-antialiased;
  transform: translateZ(0);
  perspective: 1000px;
}

Result: Still flickering. Sometimes even worse.

Attempt 3: Reference astro-sphere’s Simple Implementation

I found the astro-sphere project, which runs smoothly on Safari. After comparison, I found its animation implementation was simpler:

/* astro-sphere implementation */
.animate {
  opacity: 0;
  transform: translateY(50px);
  transition: opacity 1s ease, transform 1s ease;
}

No GPU “optimizations” at all. I copied this approach.

Result: Still flickering. The problem wasn’t in CSS.


Phase 2: Discovering the Real Problem

Animation Was Triggered Twice

Through debugging, I found the animate() function was being called twice:

document.addEventListener("DOMContentLoaded", () => init());
document.addEventListener("astro:after-swap", () => init());

Each init() would call animate(), causing animations to be interrupted and restarted halfway through.

I added checks to prevent duplicate triggers:

function animate() {
  const animateElements = document.querySelectorAll(".animate");
  animateElements.forEach((element, index) => {
    if (element.classList.contains("show")) {
      return; // Skip elements that have already been animated
    }
    setTimeout(() => {
      element.classList.add("show");
    }, index * 150);
  });
}

Result: Problem persisted. The double triggering wasn’t the root cause.

Truth Revealed: Safari Compatibility Issues with View Transitions API

After searching Astro’s GitHub Issues, I discovered this is a known issue. Many people have reported various problems with ClientRouter/ViewTransitions on Safari:

IssueProblem Description
#8625iOS Safari 17 scroll stuttering, CPU overload, phone heating
#8711Dark mode + View Transitions = flickering
#8803Mobile device UI flickering
#9650Firefox and Safari animation jumping

And I discovered that astro-sphere runs smoothly because it commented out ViewTransitions:

<!-- <ViewTransitions /> -->

The root cause of the problem was Safari’s incomplete support for the View Transitions API.


Phase 3: Seeking Solutions

Solution A: Use CSS to Disable Safari’s View Transition Animations

Try to disable View Transition animations on Safari while keeping SPA navigation:

@supports (-webkit-hyphens: none) {
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Result: Still flickering. Cannot be solved at the CSS level.

Solution B: Disable ClientRouter on Safari

Detect Safari browser and force traditional page navigation:

const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

if (isSafari) {
  document.addEventListener("astro:before-preparation", (e) => {
    e.preventDefault();
    window.location.href = e.to.href;
  });
}

Combined with instant.page to maintain preloading experience:

if (isSafari) {
  import('instant.page');
}

Result: Flickering solved! But new problem appeared - page loading became very slow.

Chrome has instant switching (SPA navigation, resources loaded only once), Safari has to wait several seconds (every time it has to reload 18 font files, giscus scripts, Pagefind scripts).

Solution C: Completely Remove ClientRouter

Like astro-sphere, completely remove ClientRouter, all browsers use traditional page navigation:

- import { ClientRouter } from "astro:transitions";
- <ClientRouter />

Result: Consistent experience across all browsers, no flickering. But page switching lost SPA’s smooth feel.

Solution D: Override startViewTransition API

Core idea: Override document.startViewTransition on Safari to directly execute the callback without triggering transition animations.

const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari && document.startViewTransition) {
  document.startViewTransition = (callback) => {
    const result = callback();
    return {
      finished: Promise.resolve(),
      ready: Promise.resolve(),
      updateCallbackDone: Promise.resolve(result),
      skipTransition: () => {}
    };
  };
}

Result: Flickering completely solved! And it retained SPA navigation (instant page switching).

But I noticed a UX issue: no visual feedback after clicking links.


Phase 4: UX Trade-offs

Perceived Speed ≠ Actual Speed

The startViewTransition override solution is technically optimal:

  • Retains SPA navigation (instant page switching)
  • Safari flickering issue resolved

But there’s a user experience problem:

  • Browser doesn’t show progress bar during SPA navigation
  • “Silent waiting” after clicks, users don’t know if system responded
  • This “stuck” feeling is more anxiety-inducing than actual wait time

While the complete ClientRouter removal solution:

  • Traditional page refresh, browser automatically shows progress bar
  • Users immediately know “click worked, it’s loading”
  • Psychologically easier to accept waiting

Conclusion: Even with the same actual loading time, the version with a progress bar makes users feel it’s faster and more controllable.

Final Solution: Override API + Custom Loading Indicator

To get the best of both worlds, I chose:

  1. Override startViewTransition API on Safari (solve flickering, retain SPA performance)
  2. Add custom top loading progress bar (provide visual feedback)
// Safari detection: completely disable View Transitions API
(function() {
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  if (isSafari && document.startViewTransition) {
    document.startViewTransition = (callback) => {
      const result = callback();
      return {
        finished: Promise.resolve(),
        ready: Promise.resolve(),
        updateCallbackDone: Promise.resolve(result),
        skipTransition: () => {}
      };
    };
  }
})();

Some Insights

1. Problems Might Be at a Deeper Level

CSS animation optimization couldn’t solve the problem because the root cause was Safari’s buggy implementation of the View Transitions API.

2. Referencing Other Projects Is a Shortcut

When I saw astro-sphere directly commented out ViewTransitions, I realized this might be a known issue, no need to obsess over “fixing” Safari.

3. Search GitHub Issues

Many problems have been encountered by others. Astro’s GitHub Issues has lots of discussions about Safari compatibility.

4. UX Is More Important Than Technology

The technically optimal solution isn’t necessarily the best user experience solution. Sometimes “slow but with feedback” is better than “fast but without feedback”.

6. Browser Compatibility Is Always a Pain Point

Most component libraries still only optimize for Chromium due to its high market share, ignoring WebKit’s Safari and Firefox. I’m sure everyone has experienced websites that are laggy on Firefox and Safari but very smooth on Chromium browsers.

View Transitions API is a very new standard:

  • Chrome 111+ (March 2023) native support
  • Safari 18+ (September 2024) started supporting
  • Firefox still doesn’t fully support it

Even though Safari 18 “supports” this API, it doesn’t mean the implementation is perfect.



In Closing

This debugging session took me an entire day, from initially thinking it was a simple CSS problem to finally discovering it was a browser API compatibility issue.

When facing browser compatibility problems, rather than spending lots of time trying to “fix” browser bugs, it’s better to choose graceful degradation - let problematic browsers use simpler, more stable solutions.