Navigation latency kills user experience. A 200ms delay between clicking a link and seeing the first paint feels sluggish, even though it's barely perceptible in isolation. Link preloading—fetching route code and data before the user actually navigates—closes that gap and makes SvelteKit applications feel instant. In this guide, we'll explore how to implement link preloading strategies that work across different scenarios and performance budgets.
What is link preloading?
Link preloading is the practice of loading route code, data, or both before the user navigates to that route. SvelteKit provides built-in preload hooks that run when a user hovers over, taps, or focuses a link. The framework handles the network request and code splitting overhead behind the scenes, so by the time the user completes the navigation action, the browser has already fetched everything it needs.
There are two main scenarios to consider:
- Eager preloading: Load data and code upfront for critical paths (e.g., checkout, dashboard).
- On-demand preloading: Load only when the user signals intent (hover, tap, focus).
The balance between these strategies determines whether your app feels snappy or wastes bandwidth on pages users never visit.
Preload data on hover or tap
The most common preloading pattern is triggering loads when users hover over a link on desktop or press down on mobile. SvelteKit's data-sveltekit-preload-data attribute makes this trivial.
Consider an e-commerce product listing. Each product card links to a detail page that requires fetching inventory, reviews, and recommendations. Without preloading, users see a loading spinner when they click. With preloading on hover, the data is usually ready by the time they click.
<!-- src/routes/products/+page.svelte -->
<script>
export let data;
</script>
<div class="products">
{#each data.products as product (product.id)}
<a
href="/products/{product.id}"
data-sveltekit-preload-data="hover"
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</a>
{/each}
</div>The data-sveltekit-preload-data="hover" attribute tells SvelteKit to call the destination route's load function when the user hovers. On touch devices, it triggers on touchstart instead. This gives you 100–300ms of buffer time before the actual navigation click.
For links where you want more aggressive preloading, use data-sveltekit-preload-data="tap". This fires the moment the user presses down (before release), which is useful for critical flows but should be used sparingly to avoid wasted requests on accidental hovers.
<!-- Example: checkout button -->
<a href="/checkout" data-sveltekit-preload-data="tap">
Go to Checkout
</a>If you want preloading to apply to all links in a section without repeating the attribute, wrap them in a container with data-sveltekit-preload-data:
<nav data-sveltekit-preload-data="hover">
<a href="/dashboard">Dashboard</a>
<a href="/settings">Settings</a>
<a href="/team">Team</a>
</nav>Preload code eagerly for key links
Data preloading is only half the story. SvelteKit also splits your route components into separate bundles. If a user navigates to a route and the component code hasn't been downloaded yet, they'll see a delay before the component hydrates and renders. For high-traffic routes, preloading the JavaScript bundle makes sense.
Use data-sveltekit-preload-code to download route code without calling the load function:
<a href="/dashboard" data-sveltekit-preload-code="eager">
Dashboard
</a>The "eager" value loads code as soon as the page renders (via a prefetch link tag). This is useful for routes you're confident users will navigate to soon. The alternative is "tap", which loads code when the user presses down on the link, similar to data preloading.
A real-world scenario: you run a SaaS product with a navigation sidebar. The dashboard is the primary route after login. On the login success page, preload the dashboard code eagerly:
<!-- src/routes/login/+page.svelte (after successful login) -->
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
onMount(async () => {
// Assume user was just logged in
const response = await fetch('/api/auth/me');
if (response.ok) {
goto('/dashboard');
}
});
</script>
<!-- Sidebar that preloads dashboard code -->
<nav>
<a href="/dashboard" data-sveltekit-preload-code="eager">
Dashboard
</a>
<a href="/reports" data-sveltekit-preload-code="tap">
Reports
</a>
</nav>You can combine both attributes on the same link to preload both data and code:
<a
href="/products/{id}"
data-sveltekit-preload-data="hover"
data-sveltekit-preload-code="tap"
>
View Product
</a>Disable preloading for heavy pages
Preloading is powerful, but it's not free. If you preload too aggressively, you'll waste bandwidth on pages users never visit, especially on mobile or metered connections. Some routes are expensive to load—complex dashboards with heavy data fetches, file uploads, or real-time subscriptions.
Disable preloading on links to heavy routes using data-sveltekit-preload-data="off":
<!-- Example: large report with lots of data -->
<a href="/reports/annual" data-sveltekit-preload-data="off">
Annual Report
</a>In your load function, you can also disable preloading for the entire route by returning noDataRequired: true (or returning false from shouldLoad if you want to skip loading entirely until navigation):
// src/routes/reports/annual/+page.server.ts
export async function load({ fetch }) {
// This is expensive; don't preload it
const report = await fetch('/api/reports/annual');
return {
report: await report.json(),
};
}
export const preload = false; // Disable all preloading for this routeA common mistake is leaving preloading enabled on routes that trigger side effects (subscriptions, tracking, webhooks). If the preload request isn't the same as the navigation request, you might double-count analytics or trigger duplicate mutations. Always audit your load functions to ensure they're idempotent or wrap side effects in navigation guards.
Configure preloading on body or links
You can set default preloading behavior globally using the data-sveltekit-preload-data attribute on the document body. This is useful for applications where preloading is the norm, and you opt out on specific links:
<!-- src/app.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>My App</title>
</head>
<body data-sveltekit-preload-data="hover">
<div id="svelte"></div>
</body>
</html>Now all links default to preloading on hover. Override per-link as needed:
<!-- Inherits preload-data="hover" from body -->
<a href="/dashboard">Dashboard</a>
<!-- Override: disable preloading -->
<a href="/reports/annual" data-sveltekit-preload-data="off">
Annual Report
</a>
<!-- Override: more aggressive -->
<a href="/checkout" data-sveltekit-preload-data="tap">
Checkout
</a>For code preloading, you can do the same on the body with data-sveltekit-preload-code, though the default is usually "tap" (load code when pressed, not eagerly). Change it to "eager" globally if your app is small and you want to load everything upfront:
<body data-sveltekit-preload-code="eager">
<div id="svelte"></div>
</body>In production SaaS and e-commerce apps, we typically use data-sveltekit-preload-data="hover" globally and selectively upgrade to "tap" for checkout or high-value actions. Code preloading stays at "tap" by default to avoid bloating the initial payload on slower networks.
Common mistakes
- Preloading routes with side effects: If your
loadfunction sends analytics pings, increments counters, or updates state, preloading will trigger those effects before navigation. Always makeloadfunctions idempotent or move side effects to+page.sveltewhere you can check if navigation actually occurred. - Aggressive preloading on all routes: Preloading every link aggressively wastes bandwidth and data. Use
"tap"or"hover"strategically, and disable on heavy routes. A/B test your preload strategy if you're unsure. - Forgetting mobile users: On touch devices,
"hover"becomes"touchstart", which fires immediately when the user's finger touches the screen. This can feel jumpy if you're not careful. Test on real devices, not just desktop browsers. - Not combining with layout-level preloading: If your navigation structure spans multiple layouts, preloading might only apply to the current layout's links. Use the body-level configuration to ensure consistency across the app.
- Ignoring network conditions: SvelteKit doesn't preload on slow connections by default, but you should monitor network usage in production. Tools like Chrome DevTools Network tab and WebPageTest can help identify preload misconfigurations.
Key takeaways
- Use
data-sveltekit-preload-data="hover"on links where you want instant navigation after the user clicks, with minimal setup. - Use
data-sveltekit-preload-code="eager"for high-traffic routes to avoid component hydration delays. - Disable preloading on heavy routes or routes with side effects using
data-sveltekit-preload-data="off". - Set global defaults on the
<body>element and override per-link to keep HTML clean and maintainable. - Test preloading on real devices and slow network conditions (use Chrome DevTools throttling) to ensure it actually improves perceived performance.
- Make
loadfunctions idempotent so preload requests don't have unintended side effects.






Comments
Loading comments…