How to Lazy Load Components in Svelte
Your Svelte app might be fast by default, but if you’re bundling a rich text editor, a chart library, or a complex dashboard widget into your initial JavaScript payload, users are downloading code they may never use. Component-level lazy loading in Svelte fixes this by deferring those heavy imports until they’re actually needed.
Unlike React, Svelte has no built-in lazy() helper. Instead, lazy loading relies directly on JavaScript’s native import() function combined with conditional rendering. It’s more manual, but it’s also more flexible.
Key Takeaways
- Svelte lacks a built-in
lazy()helper, so lazy loading uses JavaScript’s nativeimport()with conditional rendering - SvelteKit handles route-level code splitting automatically, but component-level lazy loading is still necessary for heavy widgets within a single page
- Vite splits dynamic imports into separate chunks with zero configuration
- Three core patterns cover most use cases: load on demand with
{#await}, preload on hover, and defer until the browser is idle
Why Component-Level Lazy Loading Still Matters in SvelteKit
SvelteKit already handles route-level code splitting automatically. Each route gets its own chunk, so navigating between pages doesn’t bloat the initial bundle. But within a single page, everything you import at the top of a .svelte file is bundled together and loaded upfront.
That’s where component-level lazy loading becomes valuable. If a page contains a heavy chart component, a video player, or a modal that only appears after user interaction, there’s no reason to load that code on page arrival.
SvelteKit builds with Vite, which handles dynamic imports natively. When Vite sees import('./HeavyChart.svelte'), it automatically splits that module into a separate chunk. No extra configuration needed.
How to Lazy Load Svelte Components: Core Patterns
Basic Lazy Loading with {#await}
The simplest approach uses Svelte’s {#await} block directly in the template:
<!-- src/routes/+page.svelte -->
<script>
let showChart = false;
let chartData = [1, 2, 3];
</script>
<button onclick={() => showChart = true}>Load Chart</button>
{#if showChart}
{#await import('$lib/components/HeavyChart.svelte')}
<p>Loading chart...</p>
{:then Chart}
<Chart.default data={chartData} />
{:catch error}
<p>Failed to load chart: {error.message}</p>
{/await}
{/if}
This works in both Svelte 4 and Svelte 5. The {:catch} block is important — network failures happen, and silent errors make debugging painful. In Svelte 4, dynamically switching components typically uses <svelte:component this={Component}>. In modern Svelte, changing the component reference can also trigger re-rendering directly.
The browser caches the module after the first import. Subsequent renders of the same component don’t trigger a new network request.
Discover how at OpenReplay.com.
Load on Hover for Faster Perceived Performance
A common pattern is to start loading when the user signals intent — hovering over a trigger element:
<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { Component } from 'svelte';
let HeavyWidget: Component<{ message: string }> | null = $state(null);
async function loadWidget() {
if (!HeavyWidget) {
const module = await import('$lib/components/HeavyWidget.svelte');
HeavyWidget = module.default;
}
}
</script>
<div onmouseenter={loadWidget}>
<p>Hover to preload the widget</p>
</div>
{#if HeavyWidget}
<HeavyWidget message="Ready!" />
{/if}
This works well for tooltips, popovers, and sidebars. The component starts loading before the user clicks, so by the time they interact, it’s often already cached.
Load When the Browser Is Idle
For non-critical components that improve the page but aren’t immediately needed, use requestIdleCallback:
<script lang="ts">
import { onMount } from 'svelte';
import type { Component } from 'svelte';
let FeedbackWidget: Component | null = $state(null);
onMount(() => {
const load = () =>
import('$lib/components/FeedbackWidget.svelte').then(
(m) => (FeedbackWidget = m.default)
);
if ('requestIdleCallback' in window) {
requestIdleCallback(load);
} else {
setTimeout(load, 300); // fallback for Safari
}
});
</script>
{#if FeedbackWidget}
<FeedbackWidget />
{/if}
Note that Safari support for requestIdleCallback has historically been inconsistent, so the setTimeout fallback is recommended.
When Not to Lazy Load
Not every component benefits from lazy loading. Avoid it for:
- Above-the-fold UI — navigation, hero sections, primary content
- Small components — the async overhead outweighs the savings
- Components needed immediately on mount — the loading flash degrades UX
Over-splitting creates many small async chunks. Modern bundlers like Vite handle this efficiently, but there’s still a point of diminishing returns.
Conclusion
Lazy loading Svelte components comes down to one thing: using import() instead of a static import, then rendering the result conditionally. SvelteKit’s Vite-based build handles the code splitting automatically. The three patterns above — on-demand, on-hover, and on-idle — cover most real-world scenarios. Pick the trigger that matches when users actually need the component, add error handling, and your initial bundle stays lean.
FAQs
Dynamic import works in both versions. In Svelte 4, you typically render the loaded component using svelte:component with the this attribute. In Svelte 5, you can use the resolved component directly as a tag in the template. The underlying import mechanism is the same in both cases.
Dynamic imports execute during SSR as well, since Node.js supports them. However, lazy loading is primarily a client-side optimization aimed at reducing the browser's initial JavaScript payload. If a component must render on the server for SEO or first-paint reasons, a static import is the better choice.
Check your build output or use a bundle analyzer to identify large chunks. Components that pull in heavy third-party libraries like chart renderers, rich text editors, or map widgets are strong candidates. If a component adds less than a few kilobytes, the overhead of an extra network request likely outweighs the savings.
No. The browser caches the module after the first dynamic import resolves. On subsequent renders, the cached module is returned immediately, so the loading state appears only on the initial fetch. You can also preload the module on hover or during idle time to eliminate the visible delay entirely.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.