Replies: 2 comments
|
I’ve seen a few people ask about this before (here’s a userland proof-of-concept I coded for someone to do something similar, although more limited than what a built-in option could do). It’s mainly for really high traffic and complex sites, but the API described here sounds like a nice approach for those who do need it. |
|
Cool idea! One potential issue I guess is that the outer parts need to have shorter cache life than the inner parts or it doesn't do what you want. For example, if the ProductGrid caches for 2 days but the page caches for 3 days, then the ProductGrid only updates after the page does. Am I thinking about this right, or were you thinking there would be something that stitched these things together? And then I guess related, is this a problem for the CMS use-case? I imagine you are going to want to give a short cache life for the listing and article parts, and longer for the shell. Can you do that with this model? |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
A new island primitive: cached islands, using a new
server:cachedirective. A cached island is server rendered once, stored, and replayed into subsequent responses until it expires or is invalidated. The page around it stays fully dynamic and rendered on-demand, while the expensive shared subtrees inside it stop being rendered per request.This is the inverse of server islands: they are dynamic holes in a static shell, this is durable fragments in a dynamic shell. Slots are per-request, so islands can nest: dynamic holes inside cached fragments inside dynamic pages.
Background & Motivation
Astro provides several ways of speeding up responses, using caching and prerendering. Prerendering and route caching store the whole page, while server islands allow dynamic islands inside a static shell.
Prerendering is all-or-nothing per route: anything that requires personalisation, customisation or anything else dynamic means the whole tree must be rendered on-demand, whether as a server island or as an on-demand route. The new route caching feature solves this at the response level, but a response cache is keyed by URL: a page whose shell varies per user either can't be response-cached. Those are exactly the pages whose expensive subtrees are shared – the product grid, the CMS fragment, the mega-nav.
Server islands can in theory be cached, but this is not ergonomic and will still need to be loaded separately from the shell, meaning that the cached part perversely appears slower than the dynamic one.
Several of the largest Astro deployments (incuding major ecommerce and news sites) already solve this by hand: render a fragment, stash the HTML in a KV or blob store, replay it with
set:html, invalidate by TTL or webhook. Everyone rebuilds the same plumbing, and maintains a complex architecture that is only sustainable for the largest organisations. Also, everyone hits the same bugs – because a rendered Astro fragment is not representable as an HTML string. The runtime interleaves markup with render instructions (hydration bootstrap, framework renderer scripts, CSP hash registration) that are deduplicated per response; cache a string and you double-emit or drop them depending on what else is on the page. Cached HTML also embeds per-build asset URLs and per-build encrypted server-island props, so entries that outlive a deploy break silently. And string caching freezes children, so anything with a personalised child cannot be cached.It is tempting to propose fixing this in userland, such as with a first-class
Cacheablecomponent. However these drawbacks are not fixable outside core, because the solution is caching render chunk streams, not HTML strings – markup plus those instructions, persisted symbolically and replayed through the normal per-response machinery. A replayed island is indistinguishable from a rendered one, which is why nested client islands, nested server islands and CSP all just work.Goals
server:cachedoes not change behaviour: identical markup, streaming, hydration and CSP. On a hit the island is just faster than plain SSR.maxAge/swr/tagsvocabulary, the sameAstro.cache/context.cacheAPI, and oneinvalidate()that clears tagged routes and tagged islands together.maxAge+ stale-while-revalidate), tag purge, key purge and automatic invalidation on deploy.cacheHints flow into island entries through the samecache.set(entry)call route caching accepts.Non-Goals
Example
Everything per-request reaches a cached island one of two ways: through props, which become part of the cache key, or through slots, which are rendered fresh on every request and inserted into the stored fragment. Reading request state directly –
Astro.request,Astro.cookies,Astro.sessionetc – inside a cached subtree will throw in the build, with a runtime guard for dynamic access.The bare boolean form
server:cacheuses configured defaults and an automatic key, derived from component identity, props, locale and a per-deploy id – optionally extended with akey. User keys can't collide across components, locales or deploys, and every deploy opens a fresh namespace, preventing stale-asset bugs.Inside a
server:cachesubtree,Astro.cacheis island-scoped with the same semantics as route caching:set({ maxAge, swr, tags })configures the entry,set(liveDataEntry)uses a live collection's cache hints, andset(false)opts this render out entirely. Components callingcache.set()become scope-agnostic: the same code declares cacheability whether it's the whole response or one island of it.Invalidation uses the same API as route caching: one call clears both layers:
Configuration nests inside the existing
cacheblock. Adapters supply platform defaults, so this is optional:Storage is the web Cache API (
caches.open()) – native on Cloudflare, Deno and Netlify, with pluggable providers everywhere else – with entries regional and tag state global, and an optional tiered mode that backs regional caches with a global store so an expensive fragment renders once worldwide. In development, storage is a noop like route caching: islands render fresh every time and the toolbar shows each island's derived key, tags and freshness instead.The directive is implemented only in the experimental Rust compiler, so this proposal requires
experimental.rustCompiler.All reactions