The Data Freshness Problem

A SvelteKit application loads data when a user navigates to a route. The load function runs, the data arrives, and the page renders. So far so good. But applications are not static. A user submits a form. A record gets updated. Another user’s action changes shared state. A background timer fires. In all of these situations, the page already has data, and that data is now stale. The question is: how do you get the page to reload its data without a full browser navigation?

The answer lives in SvelteKit’s dependency tracking system. Every load function passively records which pieces of state it read during execution. When any of those recorded inputs change, SvelteKit reruns the load function and updates the page data in place, without a full page reload, and without discarding component state that has nothing to do with the changed data.

This system is the bridge between SvelteKit’s server-side data loading model and the live, reactive feel that users expect from a well-built web application. Understanding it precisely means you can control when data is fresh, when it is stale, and exactly what it costs to refresh it. Getting it wrong means either stale data that users cannot trust or over-eager refreshes that hammer your server for no reason.

This article covers every rerun trigger in the system, how each one is recorded, and how to work with them deliberately. It ends with a complete live-updating dashboard example that puts every concept into practice.


Why Automatic Reruns Are Not Enough

Consider a user on a posts listing page. The URL is /app/posts?status=published&page=1. They click a button that archives one of the posts. Your +page.server.ts load function returned the current list on navigation. That list is now wrong. The archived post should be gone.

The naive solution is window.location.reload(). That works, but it destroys all client-side state, scrolls the user to the top, and causes a full round-trip that re-executes every layout and page load function in the route hierarchy even if only the post list changed.

SvelteKit’s invalidate() function provides a surgical alternative. You tell SvelteKit which dependency became stale. It reruns only the load functions that depend on that thing, merges the new data, and updates the components that care. Scroll position is preserved. Layout state is untouched. The update is as minimal as it can be.

The challenge is that this system requires you to understand what a “dependency” is in SvelteKit’s model, which inputs are tracked automatically, and when you need to declare a dependency explicitly rather than relying on automatic tracking.


What SvelteKit Tracks Automatically

Every load function receives an event object with several inputs. SvelteKit wraps each input with tracking logic. When a load function reads a tracked input, that input is registered as a dependency for that load function. If the input changes between navigations, the load function reruns.

The automatically tracked inputs are params, url, route, and the output of parent().

params

params tracks only the keys you read. If a load function reads params.slug, then navigating to a different slug reruns it. If only another param changes and slug is unchanged, this load does not rerun.

url

url dependencies are also read-based and granular. Reading url.pathname tracks path changes. Reading url.searchParams.get('page') tracks that query key. Reading raw url.search tracks the full query string, which is broader than most loads need.

route

route.id is tracked if read. This is less common than params and url, but still useful for route-pattern-specific logic like analytics bucketing.

If you want the full URL-data mechanics and implementation patterns (params, url.searchParams, route.id, SSR hash behavior, pagination/filter examples), see Using URL Data in Load Functions. This article stays focused on how those reads affect rerun behavior.

parent()

When a load function calls await parent(), the result of the ancestor layout loads is registered as a dependency. If any ancestor layout load reruns, every descendant load function that called await parent() reruns in turn. The cascade propagates down through the layout hierarchy to every load function that declared the parent dependency; there is no data-comparison short-circuit.


One implication of the tracking model is worth stating explicitly. When a user navigates between two pages that share a layout, the layout load functions do not automatically rerun. The URL changed, but the layout route did not. The layout’s data is reused.

If the layout load function did read url.searchParams or a specific param, and those changed, then the layout load function would rerun. Otherwise it is skipped.

This is the correct behavior for most layouts. The navigation header, the sidebar, the user information in the shell: these things do not change when the user clicks from /app/posts to /app/analytics. Skipping the layout load function means the user sees the new page data without an unnecessary round-trip for data they already have.

Loading diagram...

This selective rerun behavior is what makes SvelteKit navigation feel fast even when layouts contain their own data fetches.


Manual Invalidation

Automatic reruns cover navigation-driven state changes. They do not cover mutations. When a user archives a post, the URL has not changed, the params have not changed, and no automatic trigger fires. The data is stale, but SvelteKit has no way to know that.

Manual invalidation is the tool for this case. SvelteKit exposes two client-side functions from $app/navigation: invalidate(url) and invalidateAll().

invalidate(url)

invalidate(url) accepts a URL string or a URL object. It marks any load function that depends on that URL as stale and reruns it. There is a critical distinction here: this fetch-based URL tracking only applies to universal load functions (+page.js, +layout.js). Server load functions (+page.server.ts, +layout.server.ts) never automatically track fetched URLs — doing so would risk leaking server-side secrets to the client. For server load functions, depends() is the only mechanism available for manual invalidation.

For universal load functions, “depends on that URL” means the load function passed that exact URL to the SvelteKit fetch wrapper during execution. SvelteKit records every URL passed to its fetch, and invalidate(url) triggers reruns for all universal load functions that have that URL in their recorded fetch set.

<!-- src/routes/app/posts/+page.svelte (client-side action handler) -->
<script lang="ts">
	import { invalidate } from '$app/navigation'

	async function archivePost(postId: string) {
		const response = await fetch(`/api/posts/${postId}/archive`, { method: 'POST' })

		if (response.ok) {
			// The posts list is now stale. Tell SvelteKit which URL to invalidate.
			// This reruns any universal load function that fetched this URL.
			// For a server load function, use depends() + invalidate('app:identifier') instead.
			await invalidate('/api/posts')
		}
	}
</script>

The URL matching used by invalidate is exact by default, but you can pass a function instead of a string to perform custom matching:

// Match any URL that starts with '/api/posts'.
// This reruns load functions that fetched '/api/posts',
// '/api/posts?status=published', '/api/posts/my-slug', and so on.
await invalidate((url) => url.pathname.startsWith('/api/posts'))

The function form gives you precise control when a single endpoint serves multiple filtered variants and you want to invalidate all of them with one call.

invalidateAll()

invalidateAll() reruns every load function in the current route hierarchy, regardless of what they fetched. It is the heavy hammer. Every layout load and the page load all rerun.

import { invalidateAll } from '$app/navigation'

// After a significant state change, refresh everything.
// Use this sparingly. It causes every load function in the route to rerun.
await invalidateAll()

invalidateAll() is appropriate when a change is so broad that you cannot predict which specific URLs need invalidation. Switching the active workspace in a multi-tenant app. Changing the authenticated user. Toggling a feature flag. Situations where the scope of change is system-wide rather than endpoint-specific.

For targeted mutations, prefer invalidate(url). For broad changes, invalidateAll() is the right tool with the understanding that it is more expensive.


Declaring Custom Dependencies with depends()

invalidate(url) works when your load function fetches a URL that you can reference from the call site. But not all dependencies are URLs. Sometimes the data a load function returns is derived from a database query, a calculation, a time window, or some application-level concept that has no single URL to point at.

depends('app:identifier') solves this problem. It declares a named, non-URL dependency. The identifier must be prefixed with one or more lowercase letters followed by a colon, conforming to the URI specification, for example app:dashboard, posts:list, or user:session. Schemes like app: are the conventional choice.

Calling depends('app:posts') inside a load function registers app:posts as a dependency of that load function. Calling invalidate('app:posts') from anywhere in the client will then trigger a rerun of any load function that declared that dependency.

// src/routes/app/dashboard/+page.server.ts
// The dashboard data depends on a concept called 'app:dashboard-metrics'.
// This is not a URL. It represents the broader concept of dashboard data freshness.
// Any client-side code that knows dashboard data has changed can call
// invalidate('app:dashboard-metrics') to trigger a rerun.

import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async ({ depends, fetch }) => {
	// Register a named dependency for this load function.
	// The load function will rerun whenever invalidate('app:dashboard-metrics')
	// is called from the client, regardless of what URLs it fetches.
	depends('app:dashboard-metrics')

	const [metricsResponse, activityResponse] = await Promise.all([
		fetch('/api/dashboard/metrics'),
		fetch('/api/dashboard/recent-activity')
	])

	return {
		metrics: metricsResponse.ok ? await metricsResponse.json() : null,
		activity: activityResponse.ok ? await activityResponse.json() : []
	}
}
<!-- src/routes/app/dashboard/+page.svelte -->
<!-- The refresh button triggers a rerun via the named dependency. -->

<script lang="ts">
	import { invalidate } from '$app/navigation'

	async function refreshDashboard() {
		await invalidate('app:dashboard-metrics')
	}
</script>

<button onclick={refreshDashboard}>Refresh</button>

depends() is the correct tool when you want to decouple the invalidation trigger from the specific URLs being fetched. It allows load functions to evolve (changing which URLs they fetch) without requiring changes to the invalidation call sites.

Multiple load functions can declare the same dependency. If two different load functions both call depends('app:user-data'), then invalidate('app:user-data') will trigger both of them to rerun. This allows coordinated invalidation across the load hierarchy without each call site needing to know which specific load functions are involved.


Opting Out of Tracking with untrack()

The tracking system is valuable, but there are cases where you want a load function to read a value without creating a dependency on it. The classic example is reading a value for logging or telemetry purposes: you want to include it in a request header or a log message, but you do not want the load function to rerun when that value changes.

untrack(fn) accepts a function and executes it without registering any dependencies on inputs read inside the function. The return value is whatever the function returns.

// src/routes/app/posts/[slug]/+page.server.ts
// The load function depends on params.slug to fetch the post.
// It reads url.searchParams.get('ref') for analytics, but the page
// should not rerun when the referring source changes. untrack prevents
// the ref parameter from becoming a dependency.

import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async ({ params, url, untrack, fetch }) => {
	// Read the referring source without tracking it.
	// Changing ?ref=newsletter vs ?ref=twitter does not trigger a rerun.
	const referrer = untrack(() => url.searchParams.get('ref'))

	const response = await fetch(`/api/posts/${params.slug}`)

	if (!response.ok) {
		error(404, 'Post not found')
	}

	const post = await response.json()

	if (referrer) {
		// Fire-and-forget analytics recording.
		// The SvelteKit event fetch supports relative URLs on the server and works correctly here
		// because the response is never consumed; SSR capturing only hooks into .json()/.text(),
		// which are never called. Not awaited because analytics must never block the page render.
		fetch('/api/analytics/referrer', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ postId: post.id, referrer })
		}).catch(() => {
			// Analytics failure should never break page load.
		})
	}

	return { post }
}

Without untrack, the load function would rerun every time the ref query parameter changed. With untrack, it only reruns when params.slug changes, which is the correct behavior.

untrack is also useful when reading configuration or environment-derived values that you know will not change during a session. Reading a feature flag that is fixed per deployment, or reading a locale identifier that only changes on an explicit user action you handle separately, can both be wrapped in untrack to prevent spurious reruns.


Every Situation That Triggers a Rerun

With all of the mechanisms covered, it helps to have a complete reference of every condition that triggers a load function to rerun.

Loading diagram...

There are two additional implicit triggers. Calling goto(url, { invalidateAll: true }) from $app/navigation behaves like navigation followed by invalidateAll().

For surgical invalidation within a goto call, the invalidate array option accepts the same arguments as invalidate() — strings, URL objects, or predicate functions — and triggers only the matching load functions rather than everything: goto('/somewhere', { invalidate: ['/api/posts'] }).

Using enhance on a <form> element will also call invalidateAll() by default after a successful form submission, which is why Svelte form actions update page data automatically.


Practical Example: A Live-Updating Dashboard

The scenario is a dashboard that shows key metrics. The metrics should refresh automatically every 30 seconds without any user action. The user can also click a refresh button to fetch immediately. Both the automatic and manual refresh paths use the same invalidation mechanism.

Load function with named dependency

// src/routes/app/dashboard/+page.server.ts

import type { PageServerLoad } from './$types'

export interface DashboardMetrics {
	activeSessions: number
	postsPublishedToday: number
	totalReadsThisWeek: number
	averageEngagementRate: number
	lastUpdated: string
}

export interface RecentActivity {
	id: string
	type: 'post_published' | 'comment_received' | 'milestone_reached'
	message: string
	occurredAt: string
}

export const load: PageServerLoad = async ({ depends, fetch }) => {
	// Declare the named dependency. This load function reruns whenever
	// invalidate('app:dashboard') is called from the client.
	// The load function also reruns if the URL or params change (none here),
	// or if invalidateAll() is called.
	depends('app:dashboard')

	const [metricsResponse, activityResponse] = await Promise.all([
		fetch('/api/dashboard/metrics'),
		fetch('/api/dashboard/activity?limit=10')
	])

	const metrics: DashboardMetrics | null = metricsResponse.ok ? await metricsResponse.json() : null

	const activity: RecentActivity[] = activityResponse.ok ? await activityResponse.json() : []

	return { metrics, activity }
}

The dashboard component with auto-refresh

<!-- src/routes/app/dashboard/+page.svelte -->

<script lang="ts">
	import { onMount } from 'svelte'
	import { invalidate } from '$app/navigation'
	import type { PageProps } from './$types'

	let { data }: PageProps = $props()

	// Track the last refresh time for display purposes.
	let lastRefreshed = $state(new Date())
	let isRefreshing = $state(false)

	// Format the last refreshed time reactively.
	// Rebuilds whenever lastRefreshed changes.
	const refreshedLabel = $derived(
		lastRefreshed.toLocaleTimeString('en-US', {
			hour: 'numeric',
			minute: '2-digit',
			second: '2-digit'
		})
	)

	// Manual refresh triggered by the button.
	// Sets isRefreshing to true, calls invalidate, then resets.
	async function refreshNow() {
		isRefreshing = true
		await invalidate('app:dashboard')
		lastRefreshed = new Date()
		isRefreshing = false
	}

	// Auto-refresh every 30 seconds.
	// onMount ensures this only runs in the browser, not during SSR.
	// The interval is cleared when the component unmounts (user navigates away),
	// preventing memory leaks and orphaned timers.
	onMount(() => {
		const interval = setInterval(async () => {
			await invalidate('app:dashboard')
			lastRefreshed = new Date()
		}, 30_000)

		return () => clearInterval(interval)
	})

	// Format a number as a compact string: 1200 -> '1.2K', 1500000 -> '1.5M'
	function formatCount(n: number): string {
		return new Intl.NumberFormat('en-US', {
			notation: 'compact',
			maximumFractionDigits: 1
		}).format(n)
	}

	// Format an ISO timestamp as a relative time label: '2 minutes ago'
	function relativeTime(isoString: string): string {
		const diff = Date.now() - new Date(isoString).getTime()
		const minutes = Math.floor(diff / 60_000)
		if (minutes < 1) return 'just now'
		if (minutes < 60) return `${minutes}m ago`
		const hours = Math.floor(minutes / 60)
		if (hours < 24) return `${hours}h ago`
		return `${Math.floor(hours / 24)}d ago`
	}
</script>

<div class="dashboard">
	<div class="dashboard-toolbar">
		<h1 class="dashboard-title">Dashboard</h1>

		<div class="dashboard-controls">
			<span class="last-refreshed">
				Updated {refreshedLabel}
			</span>
			<button
				onclick={refreshNow}
				disabled={isRefreshing}
				class="refresh-button"
				aria-label="Refresh dashboard data"
			>
				{isRefreshing ? 'Refreshing...' : 'Refresh'}
			</button>
		</div>
	</div>

	{#if data.metrics}
		<div class="metrics-grid">
			<div class="metric-card">
				<h2 class="metric-label">Active Sessions</h2>
				<p class="metric-value">{formatCount(data.metrics.activeSessions)}</p>
			</div>

			<div class="metric-card">
				<h2 class="metric-label">Published Today</h2>
				<p class="metric-value">{data.metrics.postsPublishedToday}</p>
			</div>

			<div class="metric-card">
				<h2 class="metric-label">Reads This Week</h2>
				<p class="metric-value">{formatCount(data.metrics.totalReadsThisWeek)}</p>
			</div>

			<div class="metric-card">
				<h2 class="metric-label">Engagement Rate</h2>
				<p class="metric-value">
					{(data.metrics.averageEngagementRate * 100).toFixed(1)}%
				</p>
			</div>
		</div>
	{:else}
		<p class="metrics-unavailable">
			Metrics are temporarily unavailable. The dashboard will retry automatically.
		</p>
	{/if}

	<section class="activity-feed">
		<h2 class="activity-title">Recent Activity</h2>

		{#if data.activity.length === 0}
			<p class="activity-empty">No recent activity.</p>
		{:else}
			<ul class="activity-list">
				{#each data.activity as event (event.id)}
					<li class="activity-item activity-item--{event.type}">
						<p class="activity-message">{event.message}</p>
						<time datetime={event.occurredAt} class="activity-time">
							{relativeTime(event.occurredAt)}
						</time>
					</li>
				{/each}
			</ul>
		{/if}
	</section>
</div>

Several design decisions in this component are worth examining individually.

The isRefreshing state flag provides user feedback during the manual refresh. Without it, the button gives no indication that a request is in progress, which causes users to click multiple times. The disabled attribute prevents double-invocations.

The onMount return function is the cleanup. SvelteKit runs onMount cleanups when a component is destroyed, which happens when the user navigates away from the route. Without the cleanup, the interval would continue firing and calling invalidate against a page that no longer exists in the browser. The cleanup is not optional.

The invalidate('app:dashboard') call in both the manual and automatic paths is identical. Both the 30-second timer and the manual button trigger the exact same load function rerun through the same identifier. The load function does not care how the invalidation was triggered. It simply reruns and returns fresh data.

Adding a visual countdown

A common UX pattern for live-refreshing dashboards is showing the user how long until the next automatic refresh. This is pure client-side state that does not interact with the load function at all:

<!-- Excerpt: add this to the dashboard toolbar section above -->

<script lang="ts">
	// ... existing imports and state ...

	const REFRESH_INTERVAL_MS = 30_000

	// Countdown in seconds until the next auto-refresh.
	let secondsUntilRefresh = $state(REFRESH_INTERVAL_MS / 1000)

	onMount(() => {
		// Countdown tick runs every second.
		const countdown = setInterval(() => {
			secondsUntilRefresh -= 1
			if (secondsUntilRefresh <= 0) {
				secondsUntilRefresh = REFRESH_INTERVAL_MS / 1000
			}
		}, 1000)

		// Data refresh runs every 30 seconds.
		const refresh = setInterval(async () => {
			await invalidate('app:dashboard')
			lastRefreshed = new Date()
			secondsUntilRefresh = REFRESH_INTERVAL_MS / 1000
		}, REFRESH_INTERVAL_MS)

		return () => {
			clearInterval(countdown)
			clearInterval(refresh)
		}
	})
</script>

<!-- In the toolbar markup: -->
<span class="refresh-countdown">
	Refreshes in {secondsUntilRefresh}s
</span>

The countdown timer runs at 1-second granularity purely in component state. It does not call invalidate. The refresh timer runs at 30-second granularity and calls invalidate. Both are separate intervals with separate cleanup handles. The cleanup function returned from onMount now clears both.


Common Mistakes and Anti-Patterns

Calling invalidate() with a URL that the load function never fetched

// Avoid: invalidating a URL that no load function actually used.
// This triggers a full dependency check but finds nothing to rerun.
// If the URL is slightly wrong (trailing slash, different query string),
// nothing reruns and the stale data persists silently.

async function archivePost(postId: string) {
	await fetch(`/api/posts/${postId}/archive`, { method: 'POST' })
	await invalidate('/api/posts/') // Trailing slash mismatch. Will not match '/api/posts'.
}
// Preferred: use the exact URL string that the load function passed to fetch(),
// or use a function for flexible matching.

async function archivePost(postId: string) {
	await fetch(`/api/posts/${postId}/archive`, { method: 'POST' })

	// Exact match for the URL used in the load function.
	await invalidate('/api/posts')

	// Or use a function to match any URL under /api/posts regardless of query string.
	await invalidate((url) => url.pathname === '/api/posts')
}

When matching is uncertain, the function form is the safer choice. It makes the matching logic explicit and testable.

Using invalidateAll() for every mutation

// Avoid: calling invalidateAll() after every small mutation.
// This reruns every load function in the entire route hierarchy,
// including layout loads that have nothing to do with the changed data.

async function updatePostTitle(postId: string, newTitle: string) {
	await fetch(`/api/posts/${postId}`, {
		method: 'PATCH',
		headers: { 'Content-Type': 'application/json' },
		body: JSON.stringify({ title: newTitle })
	})
	await invalidateAll() // Reruns root layout, app layout, and page load.
	// Only the page load actually needs refreshed data.
}
// Preferred: invalidate only the specific dependency that changed.
// For universal load functions that fetched /api/posts, invalidating the URL is enough.
// For server load functions, the load function must call depends() and be invalidated
// via that identifier instead, since server loads never auto-track fetched URLs.
async function updatePostTitle(postId: string, newTitle: string) {
	await fetch(`/api/posts/${postId}`, {
		method: 'PATCH',
		headers: { 'Content-Type': 'application/json' },
		body: JSON.stringify({ title: newTitle })
	})
	await invalidate('/api/posts')
}

Forgetting to clean up the timer in onMount

<!-- Wrong: starting an interval in onMount without returning a cleanup function.
     The interval continues running after the user navigates away.
     Multiple navigations back to this route stack up multiple intervals,
     each firing invalidate() independently. This is a runtime bug, not just a style concern. -->

<script lang="ts">
	import { onMount } from 'svelte'
	import { invalidate } from '$app/navigation'

	onMount(() => {
		setInterval(async () => {
			await invalidate('app:dashboard')
		}, 30_000)
		// No return. No cleanup. Timer leaks.
	})
</script>
<!-- Correct: return the cleanup function from onMount.
     SvelteKit calls it when the component is destroyed. -->

<script lang="ts">
	import { onMount } from 'svelte'
	import { invalidate } from '$app/navigation'

	onMount(() => {
		const interval = setInterval(async () => {
			await invalidate('app:dashboard')
		}, 30_000)

		return () => clearInterval(interval)
	})
</script>

Declaring depends() conditionally

// Avoid: calling depends() inside a conditional.
// If the condition is false on the first run, the dependency is never registered.
// invalidate('app:dashboard') will not trigger a rerun.

export const load: PageServerLoad = async ({ depends, url, fetch }) => {
	const showMetrics = url.searchParams.get('view') === 'metrics'

	if (showMetrics) {
		depends('app:dashboard') // Only registered on some runs. Unreliable.
	}

	// ...
}
// Preferred: call depends() unconditionally at the top of the load function.
// Dependencies are always registered regardless of branching logic below.

export const load: PageServerLoad = async ({ depends, url, fetch }) => {
	depends('app:dashboard') // Always registered.

	const showMetrics = url.searchParams.get('view') === 'metrics'

	// ...
}

Performance and Scaling Considerations

Invalidation is cheap from SvelteKit’s perspective: it reruns only the affected load functions and updates only the components that received new data. The cost is entirely in the work those load functions do: the network requests they make, the server computation involved, and the component updates triggered by the data change in the component tree.

For auto-refreshing dashboards, the polling interval deserves deliberate attention. A 5-second interval on a page with 10,000 concurrent users generates 2 million requests per minute against your API. A 30-second interval generates 20,000 requests per minute. The difference is a factor of 100. Choose intervals that are long enough to be manageable at your traffic scale. Consider using WebSockets or Server-Sent Events for genuinely real-time data rather than polling for it, and reserve invalidate polling for data that changes on a minute or longer cadence.

When multiple load functions declare the same depends() identifier, invalidating that identifier reruns all of them concurrently. This is generally what you want, but be aware of the fan-out. If three load functions all depend on app:dashboard, one invalidate('app:dashboard') call triggers three concurrent sets of API requests. Ensure that the downstream APIs serving those requests can handle the burst.

untrack() is also a performance tool. If a load function reads many URL properties but only a few of them actually affect the data it fetches, wrapping the non-essential reads in untrack reduces the number of reruns. A load function that reads url.search without untrack will rerun whenever any query parameter changes, even ones that are irrelevant to the data being fetched. Wrapping those reads in untrack prevents spurious dependencies from forming and keeps reruns limited to the inputs that actually matter. Note that url.hash is not accessible during load at all; it is unavailable on the server, so it is never a tracking concern.


What’s Next

With invalidation understood, the remaining performance question is the inverse: rather than refreshing data after a change, can you prefetch data before the user navigates? The next article, Prefetching with preloadData and preloadCode, covers SvelteKit’s programmatic prefetching APIs, when each is appropriate, and how prefetched data integrates with the load function cache.


See Also

  • Parallel Loading and Avoiding Waterfalls — the preceding article in this series, covering how SvelteKit runs load functions concurrently and how to prevent inadvertent sequential execution through the await parent() trap and Promise.all patterns
  • Using Parent Data in Load Functions — covers parent() mechanics in detail, including exactly what data it returns and the fetch-before-parent pattern for preserving concurrency
  • Fetch in Load Functions — covers the SvelteKit fetch wrapper, the URL tracking it enables, and why it is distinct from the global fetch
  • SvelteKit Load Documentation — official reference for the full dependency tracking system, all load event properties, and the complete invalidation API
  • SvelteKit Invalidation Documentation — official reference for invalidate, invalidateAll, depends, and untrack with canonical usage examples