It feels like every single time I think a site built in Astro is ready for launch and I go test the final build thoroughly in Safari, things get weird. This is due to how Astro allows users to defer loading “islands of JavaScript” until certain triggers happen. These triggers are defined with the various client: directives on Astro or framework components.
I’d argue that the client:idle directive is one of the most common of them. It waits until the browser is no longer busy to hydrate components. Unfortunately, if two of these components import a third which makes use of a top-level await, Safari breaks down, causing the hydration to abort and leaving the component static. Something that seems to affect a lot of my sites.
This Safari bug is the reason top-level await is no longer considered a baseline feature, although it looks like the WebKit team has since fixed the bug, so maybe this post will be outdated soon. Until then, however, here’s my workaround for this issue.
Perhaps it’s just me, but I feel like it’s not very well known that the client:idle directive can take an argument. It is an object with a single property, timeout. This option gets directly passed to the internal call to window.requestIdleCallback() that the directive uses to call the hydration code.
Contrary to how it might seem when looking at functions such as window.setTimeout(), this timeout option doesn’t specify the amount of time to wait until executing the callback, but the maximum amount of time that may pass before the callback will be executed regardless of whether the browser is idle or not. As far as I understand it, this means the browser will prioritise hydrating components with a lower timeout over those with a larger one.
Despite the technical details, the most important takeaway here is that passing the timeout option allows orchestrating the hydration of components more granularly. As a consequence, setting a timeout of 500ms on a lower-priority component that imports the same module as another component with client:idle on it will not trigger the Safari bug, as the imports don’t happen at the same time.
Of course, this workaround isn’t perfect. Getting the timeout right could vary from device to device, components may be unresponsive for a while, but it all beats hydration not working at all until the fix for the bug ships to everyone.
I hope you found this short post useful. If you’ve dealt with the problem differently before or see issues with this workaround, feel free to let me know on Mastodon.