SSR framework for Svelte 5 + Bun with islands-based selective hydration
On this page
Server islands with mochi:defer
Mark a component with mochi:defer to skip it during the initial SSR pass and render it on-demand from a dedicated endpoint after the page loads. Use this for personalized fragments (avatars, cart counts) that would otherwise block the surrounding HTML from being cached.
<!-- file: src/Page.svelte -->
<UserAvatar mochi:defer userId={123} />Children of a deferred component become the fallback shown until the island resolves:
<UserAvatar mochi:defer userId={123}>
<div class="skeleton">Loading...</div>
</UserAvatar>Server island components are normal Svelte components with full access to the request context via getRequestContext() — cookies are forwarded automatically because the fetch is same-origin.
<!-- file: src/UserAvatar.svelte -->
<script>
import { getRequestContext } from 'mochi-framework';
const { cookies } = getRequestContext();
const session = cookies.get('session');
</script>
<p>Welcome back, {userName}!</p>Fetch flow
- SSR emits a
<mochi-server-island>custom element holding the fallback content; the component itself is not rendered. - Props are serialized with
devalue, HMAC-signed, and stamped onto the element assigned-props. - On
connectedCallback, the element fetches/_mochi/island/{ComponentName}?props={signedProps}(the/_mochiprefix followsassetPrefix). - The server verifies the signature, decodes the props, renders the component, and returns the HTML.
- The HTML replaces the fallback inside the custom element.
Failed fetches are retried with exponential backoff (default 5 retries, 1s–10s); pass mochi:defer={{ retries: 10 }} to override.
Combining with hydration
Apply mochi:hydrate alongside mochi:defer to fetch the island on-demand and then hydrate it for client-side interactivity:
<ShoppingCart mochi:defer mochi:hydrate items={initialItems} />Lazy server islands with mochi:defer:visible
Defer the fetch until the wrapper scrolls into view, mirroring mochi:hydrate:visible:
<UserAvatar mochi:defer:visible userId={123}>
<div class="skeleton">Loading...</div>
</UserAvatar>
<UserAvatar mochi:defer:visible={{ rootMargin: '200px' }} userId={123} />Pass rootMargin to start fetching before the island enters the viewport. rootMargin and retries can be combined: mochi:defer:visible={{ rootMargin: '200px', retries: 10 }}. Combinable with mochi:hydrate / mochi:hydrate:visible for interactive lazy islands.
Provide fallback children when using :visible so the user has something to scroll past while waiting.
Props
Props are serialized with devalue — see Passing props to islands for the full list of supported types. Server islands additionally HMAC-sign the payload and pass it as a query parameter; if the signed props exceed URL length limits (~1800 bytes), a warning is emitted.
Do NOT ship large blobs through server-island props; instead, fetch the data inside the component using getRequestContext(). Signed-prop URLs over 1800 chars trigger a runtime warning.
Signing key
Props are signed with a 32-byte key resolved at startup from process.env.MOCHI_KEY (base64url-encoded). If MOCHI_KEY is unset, Mochi generates a random key and logs a warning — fine for local dev, broken across restarts and multi-instance deploys.
# .env
MOCHI_KEY=<base64url-encoded 32-byte secret>Generate one and write it to .env with mochi-framework generate-key:
bunx mochi-framework generate-keyDo NOT commit MOCHI_KEY to version control; instead, supply it through your platform’s secret store. Generate one with openssl rand -base64 32 | tr '+/' '-_' | tr -d '='.