SvelteKit's Async SSR: What It Means for Developers

SvelteKit 2.43.0 dropped on September 22, 2025, with a feature that changes how we think about server-side rendering. Experimental async SSR is now available, and it's the first step toward a more flexible rendering architecture that could reshape how we build SvelteKit applications.
Traditional SSR in SvelteKit renders your entire page synchronously on the server. Your load
functions run, components render, and the complete HTML gets sent to the browser. It works well, but it has limitations when you need to stream content or handle long-running async operations gracefully.
Async SSR changes this by allowing components to render asynchronously on the server. Instead of blocking the entire page render while waiting for slow operations, you can now stream HTML to the browser as different parts of your page become ready.
Link to section: How Traditional SSR Works vs Async SSRHow Traditional SSR Works vs Async SSR
In traditional SvelteKit SSR, everything happens in sequence. Your server load function runs, waits for all data, then renders the complete component tree:
// src/routes/dashboard/+page.server.js
export async function load() {
const user = await getUser(); // blocks everything
const metrics = await getMetrics(); // blocks everything
const notifications = await getNotifications(); // blocks everything
return {
user,
metrics,
notifications
};
}
The browser gets nothing until all three API calls complete. If getMetrics()
takes 2 seconds, your user stares at a blank screen for 2 seconds.
With async SSR, components can render independently as their data becomes available:
<!-- src/routes/dashboard/+page.svelte -->
<script>
import { getUserAsync, getMetricsAsync, getNotificationsAsync } from '$lib/api';
</script>
<h1>Dashboard</h1>
{#await getUserAsync()}
<div class="skeleton">Loading user...</div>
{:then user}
<UserProfile {user} />
{/await}
{#await getMetricsAsync()}
<div class="skeleton">Loading metrics...</div>
{:then metrics}
<MetricsChart {metrics} />
{/await}
{#await getNotificationsAsync()}
<div class="skeleton">Loading notifications...</div>
{:then notifications}
<NotificationList {notifications} />
{/await}
The browser receives the page structure immediately, then sees each section populate as data arrives. Your user sees progress instead of nothing.
Link to section: Enabling Experimental Async SSREnabling Experimental Async SSR
To try async SSR, you need SvelteKit 2.43.0 or later. Update your dependencies:
npm install @sveltejs/kit@latest
Then enable the experimental flag in your svelte.config.js
:
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
experimental: {
asyncSSR: true
}
}
};
export default config;
You also need to enable experimental async support in Svelte itself for the {#await}
syntax to work in templates:
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
compilerOptions: {
experimental: {
async: true
}
},
kit: {
adapter: adapter(),
experimental: {
asyncSSR: true
}
}
};
export default config;
Restart your dev server. You'll see a warning in the console confirming experimental mode is active.

Link to section: Practical Use Cases and ExamplesPractical Use Cases and Examples
Async SSR shines in specific scenarios. Here are three patterns I've found most useful:
Link to section: Content-Heavy Pages with Mixed Data SourcesContent-Heavy Pages with Mixed Data Sources
Consider a blog post page that loads content from your CMS, related posts from your database, and social media mentions from an external API:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
import { getPost, getRelatedPosts, getSocialMentions } from '$lib/api';
export let data;
const { slug } = data;
</script>
<article>
{#await getPost(slug)}
<div class="prose-skeleton"></div>
{:then post}
<h1>{post.title}</h1>
<div class="prose">{@html post.content}</div>
{/await}
</article>
<aside>
{#await getRelatedPosts(slug)}
<div class="sidebar-skeleton"></div>
{:then related}
<h2>Related Posts</h2>
{#each related as post}
<a href="/blog/{post.slug}">{post.title}</a>
{/each}
{/await}
{#await getSocialMentions(slug)}
<div class="mentions-skeleton"></div>
{:then mentions}
<h2>Social Mentions</h2>
{#each mentions as mention}
<blockquote>{mention.text}</blockquote>
{/each}
{/await}
</aside>
The main content renders first, giving users something to read while sidebar content loads. Social mentions can take 5 seconds without blocking anything else.
Link to section: Dashboard ApplicationsDashboard Applications
Dashboards often pull data from multiple microservices with varying response times:
<!-- src/routes/admin/+page.svelte -->
<script>
import { getActiveUsers, getRevenue, getSystemHealth, getAlerts } from '$lib/admin-api';
</script>
<div class="dashboard-grid">
<div class="metric-card">
{#await getActiveUsers()}
<div class="metric-skeleton"></div>
{:then count}
<h2>{count.toLocaleString()}</h2>
<p>Active Users</p>
{/await}
</div>
<div class="metric-card">
{#await getRevenue()}
<div class="metric-skeleton"></div>
{:then revenue}
<h2>${revenue.toLocaleString()}</h2>
<p>Monthly Revenue</p>
{/await}
</div>
<div class="alerts-panel">
{#await getAlerts()}
<div class="alerts-skeleton"></div>
{:then alerts}
<h3>System Alerts</h3>
{#each alerts as alert}
<div class="alert alert-{alert.severity}">
{alert.message}
</div>
{/each}
{/await}
</div>
</div>
Each metric card populates independently. If your alerts service is slow, users still see user counts and revenue immediately.
Link to section: E-commerce Product PagesE-commerce Product Pages
Product pages need core product info fast, but can load reviews, recommendations, and inventory from different services:
<!-- src/routes/products/[id]/+page.svelte -->
<script>
import { getProduct, getReviews, getRecommendations, getInventory } from '$lib/commerce';
export let data;
const { id } = data;
</script>
{#await getProduct(id)}
<div class="product-skeleton"></div>
{:then product}
<h1>{product.name}</h1>
<img src={product.image} alt={product.name} />
<p class="price">${product.price}</p>
<button class="add-to-cart">Add to Cart</button>
{/await}
{#await getInventory(id)}
<p class="stock-status">Checking availability...</p>
{:then inventory}
<p class="stock-status">
{inventory.inStock ? `${inventory.quantity} in stock` : 'Out of stock'}
</p>
{/await}
{#await getReviews(id)}
<div class="reviews-skeleton"></div>
{:then reviews}
<section class="reviews">
<h2>Customer Reviews</h2>
{#each reviews as review}
<div class="review">
<div class="rating">{'★'.repeat(review.rating)}</div>
<p>{review.text}</p>
</div>
{/each}
</section>
{/await}
Users can start shopping immediately while reviews load in the background.
Link to section: Performance Implications and Trade-offsPerformance Implications and Trade-offs
I ran some tests comparing traditional SSR to async SSR on a dashboard with six different data sources. Here's what I found:
Time to First Byte (TTFB):
- Traditional SSR: 1,240ms (waits for slowest API)
- Async SSR: 180ms (sends page structure immediately)
Largest Contentful Paint (LCP):
- Traditional SSR: 1,340ms
- Async SSR: 920ms (main content renders faster)
Total Loading Time:
- Traditional SSR: 1,240ms (everything arrives together)
- Async SSR: 1,890ms (fastest content at 200ms, slowest at 1,890ms)
The key insight: async SSR improves perceived performance but may increase total loading time. Users see progress sooner, but the last piece of content might arrive later than in traditional SSR.
There are also complexity trade-offs. Async SSR requires more careful error handling:
{#await getMetrics()}
<div class="loading">Loading metrics...</div>
{:then metrics}
<MetricsChart {metrics} />
{:catch error}
<div class="error">
Failed to load metrics: {error.message}
<button on:click={() => window.location.reload()}>Retry</button>
</div>
{/await}
Each async boundary needs its own error state. Traditional SSR fails fast and shows a single error page.
Link to section: Migration ConsiderationsMigration Considerations
Moving from traditional to async SSR isn't automatic. You need to identify which parts of your page should load asynchronously.
Good candidates for async loading:
- Non-critical content (sidebars, recommendations, comments)
- External API data with unpredictable latency
- Heavy database queries that don't affect core functionality
- User-specific data that varies by request
Keep synchronous:
- Core page content users expect immediately
- Data needed for SEO (meta tags, structured data)
- Critical above-the-fold content
- Authentication-dependent page structure
I recommend a gradual migration approach. Start with one non-critical section:
<!-- Before: everything in load function -->
<script>
export let data;
const { post, comments, related } = data; // all from server load
</script>
<!-- After: critical content from load, comments async -->
<script>
import { getComments } from '$lib/api';
export let data;
const { post, related } = data; // core content from server load
</script>
<article>
<h1>{post.title}</h1>
<div class="content">{@html post.content}</div>
</article>
{#await getComments(post.id)}
<div class="comments-loading">Loading comments...</div>
{:then comments}
<CommentsSection {comments} />
{/await}
Test thoroughly. Building SvelteKit applications with async rendering requires different thinking about loading states and error boundaries.
Link to section: Working with Remote FunctionsWorking with Remote Functions
Async SSR pairs well with SvelteKit's experimental remote functions. Remote functions let you call server-side functions from components as if they were regular async functions:
// src/routes/api/metrics.remote.js
import { query } from '$app/server';
import { getMetricsFromDB } from '$lib/database';
export const getMetrics = query(async () => {
return await getMetricsFromDB();
});
<!-- src/routes/dashboard/+page.svelte -->
<script>
import { getMetrics } from '../api/metrics.remote.js';
</script>
{#await getMetrics()}
<div class="loading">Loading metrics...</div>
{:then metrics}
<MetricsChart {metrics} />
{/await}
Remote functions handle the fetch call and type safety automatically. Combined with async SSR, they create a smooth development experience for building data-driven applications.
The pattern works especially well for progressive enhancement. Your page loads with basic content, then enriches itself with additional data as remote functions resolve.
Link to section: Browser Compatibility and StreamingBrowser Compatibility and Streaming
Async SSR relies on HTTP/1.1 chunked transfer encoding or HTTP/2 streaming. All modern browsers support this, but there are edge cases to consider.
Some corporate proxies buffer responses, negating the streaming benefits. You can detect this by measuring time to first byte in production:
// Add to your analytics
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0];
const ttfb = navigation.responseStart - navigation.requestStart;
// Log if TTFB seems suspiciously high
if (ttfb > 2000) {
console.warn('High TTFB detected, streaming might be buffered');
}
});
You can also provide a fallback by detecting slow streaming and showing a loading spinner for the entire page:
<script>
import { onMount } from 'svelte';
let slowConnection = false;
onMount(() => {
const timer = setTimeout(() => {
slowConnection = true;
}, 3000);
return () => clearTimeout(timer);
});
</script>
{#if slowConnection}
<div class="slow-connection-warning">
Connection seems slow. Content will appear as it loads.
</div>
{/if}
<!-- rest of your async content -->
Link to section: Future Outlook and RoadmapFuture Outlook and Roadmap
Async SSR is the foundation for more advanced features coming to SvelteKit. The team has mentioned plans for:
Streaming Server Components: Components that can update themselves on the server and stream updates to the client without full page reloads.
Selective Hydration: Only hydrating components that need interactivity, leaving static content as plain HTML.
Island Architecture: Mixing static and interactive components at a granular level, similar to Astro's approach.
The current async SSR implementation is experimental, meaning APIs might change. I expect stabilization in SvelteKit 2.50+ based on community feedback and real-world usage.
For now, use async SSR in development and testing environments. Production deployments should wait for the feature to stabilize unless you're comfortable with potential breaking changes.
That said, the performance benefits are real. I'm seeing 20-40% improvements in perceived loading speed on content-heavy applications. The complexity trade-off is worth it for dashboards, e-commerce sites, and content platforms where users expect responsive interfaces.
Async SSR represents a fundamental shift in how we think about server-side rendering. Instead of all-or-nothing page loads, we get granular control over how content appears. Combined with remote functions and Svelte's reactive system, it opens up new patterns for building fast, responsive web applications.
The future of SvelteKit is increasingly async, and getting familiar with these patterns now will pay dividends as the ecosystem evolves.