teta.so
SvelteKitData LoadingSSRTutorial

SvelteKit Load Functions: Server vs Universal Data Loading

AI Generated

Introduction

Every web application needs to load data. Whether you are fetching blog posts from a CMS, user profiles from a database, or product listings from an API, SvelteKit provides a structured way to load data before your page renders.

SvelteKit offers two types of load functions: server load functions (+page.server.ts) and universal load functions (+page.ts). Understanding when to use each one is crucial for building performant, secure applications.

How Load Functions Work

Load functions run before your page component renders. They fetch data and return it as props to the page. Here is the simplest example:

// src/routes/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  return {
    message: 'Hello from the server!'
  };
};
<!-- src/routes/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<h1>{data.message}</h1>

The data prop in your page component contains whatever the load function returns.

Server Load Functions (+page.server.ts)

Server load functions run exclusively on the server. They are never sent to the browser, which makes them ideal for sensitive operations.

When to Use Server Load

Use +page.server.ts when you need to:

  • Query a database directly
  • Use API keys or secrets
  • Access the filesystem
  • Use server-only npm packages
  • Read cookies or set headers

Database Access

// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';

export const load: PageServerLoad = async () => {
  const posts = await db.query(`
    SELECT id, title, slug, excerpt, published_at
    FROM posts
    WHERE status = 'published'
    ORDER BY published_at DESC
    LIMIT 20
  `);

  return { posts };
};

The $lib/server/ directory is special in SvelteKit. Files inside it can only be imported by server-side code. If you accidentally try to import a server module in a universal load function or a component, SvelteKit will throw an error at build time.

Accessing Cookies and Headers

Server load functions receive a rich event object:

// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ cookies, request }) => {
  const sessionToken = cookies.get('session');

  if (!sessionToken) {
    redirect(303, '/login');
  }

  const user = await getUserFromSession(sessionToken);

  if (!user) {
    cookies.delete('session', { path: '/' });
    redirect(303, '/login');
  }

  return {
    user: {
      id: user.id,
      name: user.name,
      email: user.email
    }
  };
};

Setting Headers and Cookies

You can set response headers and cookies from server load functions:

export const load: PageServerLoad = async ({ cookies, setHeaders }) => {
  // Set cache headers
  setHeaders({
    'cache-control': 'public, max-age=60'
  });

  // Set a cookie
  cookies.set('visited', 'true', {
    path: '/',
    maxAge: 60 * 60 * 24 * 365
  });

  return { data: 'some data' };
};

Universal Load Functions (+page.ts)

Universal load functions run on both the server (during SSR) and the client (during client-side navigation). This dual execution is what makes them "universal."

When to Use Universal Load

Use +page.ts when:

  • You are calling a public API that does not require secrets
  • You want client-side navigation to skip a server round-trip
  • You need to return non-serializable data (component constructors, functions)

Calling Public APIs

// src/routes/weather/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch }) => {
  const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true');
  const weather = await response.json();

  return { weather };
};

The Special fetch

Notice that we use the fetch provided by SvelteKit (from the function parameter), not the global fetch. This special fetch has important benefits:

  • During SSR, it can make requests to your own API routes without an HTTP round-trip
  • It forwards cookies and headers from the incoming request
  • It makes relative URLs work on the server
  • It prevents duplicate requests during hydration

Always use the SvelteKit-provided fetch in load functions:

// GOOD
export const load: PageLoad = async ({ fetch }) => {
  const res = await fetch('/api/data');
  return { data: await res.json() };
};

// BAD - don't use global fetch
export const load: PageLoad = async () => {
  const res = await globalThis.fetch('/api/data');
  return { data: await res.json() };
};

Returning Non-Serializable Data

One unique advantage of universal load functions is that they can return non-serializable data like component constructors and functions:

// src/routes/[type]/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params }) => {
  let component;

  switch (params.type) {
    case 'chart':
      component = (await import('$lib/components/Chart.svelte')).default;
      break;
    case 'table':
      component = (await import('$lib/components/Table.svelte')).default;
      break;
    default:
      component = (await import('$lib/components/Default.svelte')).default;
  }

  return { component };
};

Server load functions cannot do this because data must be serialized when sent from server to client.

Layout Load Functions

Layouts can also have load functions. Data from layout loads is available to all child pages:

// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ cookies }) => {
  const theme = cookies.get('theme') || 'light';
  return { theme };
};
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';

  let { data, children }: { data: any; children: Snippet } = $props();
</script>

<div class="app" data-theme={data.theme}>
  {@render children()}
</div>

Every page under this layout has access to data.theme without needing to load it themselves.

Accessing Parent Data

Child load functions can access data from parent layouts using the parent() function:

// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ parent }) => {
  const parentData = await parent();

  // parentData contains data from layout load functions
  const userId = parentData.user?.id;

  const projects = await getProjectsForUser(userId);

  return { projects };
};

Be careful with parent() — it creates a dependency waterfall. The child load waits for all parent loads to complete. Only use it when you genuinely need parent data.

Data Flow and Waterfalls

Understanding how data flows through layouts and pages is important for performance.

Parallel Loading

By default, sibling load functions run in parallel:

+layout.server.ts  ──→  Returns layout data
+page.server.ts    ──→  Returns page data

Both start simultaneously. The page renders when both complete.

Avoiding Waterfalls

A waterfall occurs when one load function depends on another's result:

// src/routes/+layout.server.ts
export const load: LayoutServerLoad = async () => {
  const user = await getUser(); // Takes 200ms
  return { user };
};

// src/routes/dashboard/+page.server.ts
export const load: PageServerLoad = async ({ parent }) => {
  const { user } = await parent(); // Waits for layout (200ms)
  const projects = await getProjects(user.id); // Then takes 300ms
  return { projects };
  // Total: 500ms (sequential)
};

To avoid the waterfall, restructure so the page load does not depend on parent:

// src/routes/dashboard/+page.server.ts
export const load: PageServerLoad = async ({ locals }) => {
  // Access user from locals (set in hooks) instead of parent
  const projects = await getProjects(locals.user.id);
  return { projects };
  // Now layout and page load in parallel!
};

Invalidation and Reloading

SvelteKit provides tools to reload data when something changes.

invalidate()

Call invalidate() to re-run load functions that depend on a specific URL:

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

  let { data } = $props();

  async function refresh() {
    // Re-runs any load function that uses fetch('/api/posts')
    await invalidate('/api/posts');
  }

  async function refreshAll() {
    // Re-runs ALL load functions for the current page
    await invalidate(() => true);
  }
</script>

<button onclick={refresh}>Refresh Posts</button>

depends()

Mark a load function as depending on a custom identifier:

export const load: PageLoad = async ({ depends, fetch }) => {
  depends('app:posts');

  const res = await fetch('/api/posts');
  return { posts: await res.json() };
};
<script lang="ts">
  import { invalidate } from '$app/navigation';

  async function refresh() {
    await invalidate('app:posts');
  }
</script>

invalidateAll()

The nuclear option — re-runs every load function on the current page:

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

<button onclick={() => invalidateAll()}>Refresh Everything</button>

Streaming with Promises

SvelteKit supports streaming data by returning promises from server load functions. The page renders immediately with the available data, and streamed data fills in as it resolves:

// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  // This data is available immediately
  const quickData = await getQuickData();

  return {
    quickData,
    // This promise streams in later
    slowData: getSlowData(), // Note: no await!
    analytics: getAnalytics() // Also streams
  };
};

In your page, use {#await} blocks to handle the streaming:

<script lang="ts">
  let { data } = $props();
</script>

<!-- Available immediately -->
<h1>Dashboard</h1>
<p>Welcome, {data.quickData.userName}!</p>

<!-- Streams in -->
{#await data.slowData}
  <div class="skeleton">Loading recent activity...</div>
{:then activity}
  <ul>
    {#each activity as item}
      <li>{item.description}</li>
    {/each}
  </ul>
{:catch error}
  <p class="error">Failed to load activity: {error.message}</p>
{/await}

{#await data.analytics}
  <div class="skeleton">Loading analytics...</div>
{:then stats}
  <div class="stats-grid">
    <div>Visitors: {stats.visitors}</div>
    <div>Page Views: {stats.pageViews}</div>
  </div>
{:catch error}
  <p class="error">Failed to load analytics</p>
{/await}

Streaming gives users something to see immediately while slower data loads in the background. The page does not block on the slowest query.

Comparison Table

Here is a side-by-side comparison to help you decide:

Feature +page.server.ts +page.ts
Runs on server Yes Yes (SSR)
Runs on client No Yes (navigation)
Access database Yes No
Use secrets/API keys Yes No
Access cookies Yes No
Return functions No Yes
Client-side navigation Server round-trip No round-trip
Code sent to browser No Yes

Rule of Thumb

  • Default to +page.server.ts for most data loading. It is more secure and keeps sensitive code off the client.
  • Use +page.ts when you need client-side navigation performance, or when you need to return non-serializable data.
  • Combine both when needed — server load runs first, and universal load can access its data.

Summary

SvelteKit's data loading system is flexible and well-designed:

  • Server load functions (+page.server.ts) run only on the server, ideal for databases and secrets
  • Universal load functions (+page.ts) run on both server and client, ideal for public APIs
  • Layout loads provide shared data to all child routes
  • parent() accesses parent layout data (use sparingly to avoid waterfalls)
  • invalidate() and depends() give fine-grained control over reloading
  • Streaming with promises shows fast data immediately while slow data loads in the background

Mastering load functions means your SvelteKit applications will be fast, secure, and well-structured. Start with server load functions for safety, optimize with universal loads where needed, and use streaming to deliver the best possible user experience.

Next Steps

Source: Teta Engineering

This content was generated with AI from public sources. It represents analysis and commentary, not original journalism.

Build your next app with AI

Try Teta — create sites and apps with AI agents.

Get started free