Files
Job-Info/svelte-reference/08-PERFORMANCE.md
T
aewing 0b76a4b119 Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test)
- Delete old documentation, Docker files, and k8s configs
- Add comprehensive Svelte 5 reference library (svelte-reference/)
- Update ModeSwitch orchestrator:
  - Fix port to 3005 (never use 3000/3001)
  - Add API endpoints for mode switching
  - Add bun-types and proper tsconfig
  - Clean up mode folder mappings
2026-01-21 03:56:01 +00:00

13 KiB

Svelte Performance Optimization Guide

Overview

Svelte is already highly performant by design, but there are patterns to ensure your applications stay fast as they grow.


📊 Understanding Svelte Reactivity

How Svelte 5 Reactivity Works

<script lang="ts">
  // $state creates a reactive proxy
  let items = $state<string[]>([]);
  
  // Svelte tracks which properties are READ in which contexts
  // Only those contexts re-run when properties change
  
  // This creates a dependency on items.length
  let count = $derived(items.length);
  
  // This effect only runs when items changes
  $effect(() => {
    console.log('Items changed:', items);
  });
</script>

Avoid Unnecessary Reactivity

<script lang="ts">
  // ❌ BAD: Everything is reactive
  let config = $state({
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
  });
  
  // ✅ GOOD: Static config doesn't need reactivity
  const config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
  };
  
  // ✅ GOOD: Use $state.raw for data that's replaced, not mutated
  let largeDataset = $state.raw<Record[]>([]);
  
  async function fetchData() {
    // Replace entirely instead of mutating
    largeDataset = await fetch('/api/data').then(r => r.json());
  }
</script>

🚀 Component Optimization

1. Minimize Re-renders

<script lang="ts">
  let items = $state<Item[]>([]);
  let filter = $state('');
  
  // ✅ GOOD: Computed once, cached until dependencies change
  let filteredItems = $derived(
    items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    )
  );
  
  // ❌ BAD: Recomputed on every render
  // Using inline: items.filter(...) in template
</script>

<!-- ✅ GOOD: Uses cached derived value -->
{#each filteredItems as item (item.id)}
  <ItemCard {item} />
{/each}

2. Key Your Each Blocks

<!-- ❌ BAD: No key, Svelte can't optimize updates -->
{#each items as item}
  <Item {item} />
{/each}

<!-- ✅ GOOD: Unique key enables efficient diffing -->
{#each items as item (item.id)}
  <Item {item} />
{/each}

<!-- ✅ GOOD: Index as key only for static lists -->
{#each staticLabels as label, i (i)}
  <span>{label}</span>
{/each}

3. Lazy Load Components

<script lang="ts">
  // Lazy load heavy components
  let showChart = $state(false);
  
  // Dynamic import
  async function loadChart() {
    const { Chart } = await import('./Chart.svelte');
    return Chart;
  }
</script>

{#if showChart}
  {#await loadChart() then Chart}
    <Chart data={chartData} />
  {/await}
{/if}

4. Virtualization for Long Lists

<!-- Use a virtualization library for long lists -->
<script lang="ts">
  import VirtualList from '$lib/components/VirtualList.svelte';
  
  let items = $state<Item[]>([]);
  // Could be 10,000+ items
</script>

<!-- Only renders visible items -->
<VirtualList 
  {items} 
  itemHeight={50}
  let:item
>
  <ItemRow {item} />
</VirtualList>

Simple Virtual List Implementation:

<!-- VirtualList.svelte -->
<script lang="ts" generics="T">
  import type { Snippet } from 'svelte';
  
  interface Props {
    items: T[];
    itemHeight: number;
    containerHeight?: number;
    overscan?: number;
    children: Snippet<[T, number]>;
  }
  
  let { 
    items, 
    itemHeight, 
    containerHeight = 400,
    overscan = 3,
    children 
  }: Props = $props();
  
  let scrollTop = $state(0);
  
  let totalHeight = $derived(items.length * itemHeight);
  
  let startIndex = $derived(
    Math.max(0, Math.floor(scrollTop / itemHeight) - overscan)
  );
  
  let endIndex = $derived(
    Math.min(
      items.length,
      Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
    )
  );
  
  let visibleItems = $derived(
    items.slice(startIndex, endIndex).map((item, i) => ({
      item,
      index: startIndex + i
    }))
  );
  
  let offsetY = $derived(startIndex * itemHeight);
</script>

<div 
  class="virtual-list-container"
  style:height="{containerHeight}px"
  onscroll={(e) => scrollTop = e.currentTarget.scrollTop}
>
  <div 
    class="virtual-list-content"
    style:height="{totalHeight}px"
  >
    <div style:transform="translateY({offsetY}px)">
      {#each visibleItems as { item, index } (index)}
        <div style:height="{itemHeight}px">
          {@render children(item, index)}
        </div>
      {/each}
    </div>
  </div>
</div>

<style>
  .virtual-list-container {
    overflow-y: auto;
  }
  
  .virtual-list-content {
    position: relative;
  }
</style>

Data Loading Optimization

1. Parallel Loading

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

export const load: PageLoad = async ({ fetch }) => {
  // ✅ GOOD: Parallel fetching
  const [users, posts, stats] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/stats').then(r => r.json())
  ]);
  
  return { users, posts, stats };
};

// ❌ BAD: Sequential fetching
export const loadBad: PageLoad = async ({ fetch }) => {
  const users = await fetch('/api/users').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json());
  const stats = await fetch('/api/stats').then(r => r.json());
  
  return { users, posts, stats };
};

2. Streaming with Deferred Loading

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

export const load: PageServerLoad = async ({ fetch }) => {
  // Critical data loads immediately
  const user = await fetch('/api/user').then(r => r.json());
  
  // Non-critical data streams in
  return {
    user,
    // These are promises, will stream when ready
    posts: fetch('/api/posts').then(r => r.json()),
    recommendations: fetch('/api/recommendations').then(r => r.json())
  };
};
<!-- +page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  
  let { data }: { data: PageData } = $props();
</script>

<!-- Immediate content -->
<h1>Welcome, {data.user.name}</h1>

<!-- Streamed content -->
{#await data.posts}
  <PostsSkeleton />
{:then posts}
  <PostsList {posts} />
{/await}

{#await data.recommendations}
  <RecommendationsSkeleton />
{:then recommendations}
  <Recommendations {recommendations} />
{/await}

3. Prefetching

<!-- SvelteKit auto-prefetches on hover -->
<a href="/about">About</a>

<!-- Prefetch on viewport entry -->
<a href="/products" data-sveltekit-preload-data="hover">Products</a>

<!-- Programmatic prefetch -->
<script lang="ts">
  import { preloadData } from '$app/navigation';
  
  // Prefetch when likely needed
  function handleMouseEnter() {
    preloadData('/expensive-page');
  }
</script>

🖼️ Image Optimization

Responsive Images

<picture>
  <source 
    media="(min-width: 1024px)" 
    srcset="/hero-large.webp 1x, /hero-large@2x.webp 2x"
    type="image/webp"
  />
  <source 
    media="(min-width: 640px)" 
    srcset="/hero-medium.webp 1x, /hero-medium@2x.webp 2x"
    type="image/webp"
  />
  <img 
    src="/hero-small.jpg" 
    srcset="/hero-small.jpg 1x, /hero-small@2x.jpg 2x"
    alt="Hero image"
    loading="lazy"
    decoding="async"
    width="800"
    height="400"
  />
</picture>

Lazy Loading Images

<script lang="ts">
  interface Props {
    src: string;
    alt: string;
    width: number;
    height: number;
  }
  
  let { src, alt, width, height }: Props = $props();
  
  let loaded = $state(false);
  let element: HTMLImageElement;
  
  $effect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          element.src = src;
          observer.disconnect();
        }
      },
      { rootMargin: '200px' }
    );
    
    observer.observe(element);
    
    return () => observer.disconnect();
  });
</script>

<div class="image-wrapper" style:aspect-ratio="{width}/{height}">
  <img
    bind:this={element}
    {alt}
    {width}
    {height}
    class:loaded
    onload={() => loaded = true}
  />
  {#if !loaded}
    <div class="placeholder" />
  {/if}
</div>

<style>
  .image-wrapper {
    position: relative;
    overflow: hidden;
    background: #f0f0f0;
  }
  
  img {
    opacity: 0;
    transition: opacity 0.3s;
  }
  
  img.loaded {
    opacity: 1;
  }
  
  .placeholder {
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
  }
  
  @keyframes shimmer {
    0% { background-position: -200% 0; }
    100% { background-position: 200% 0; }
  }
</style>

🔄 Effect Optimization

Debounce Effects

<script lang="ts">
  let searchQuery = $state('');
  let results = $state<SearchResult[]>([]);
  
  // ❌ BAD: Fires on every keystroke
  $effect(() => {
    fetchResults(searchQuery);
  });
  
  // ✅ GOOD: Debounced
  $effect(() => {
    const query = searchQuery; // Read reactive value
    
    const timeout = setTimeout(() => {
      if (query) {
        fetchResults(query).then(r => results = r);
      }
    }, 300);
    
    return () => clearTimeout(timeout);
  });
</script>

Avoid Heavy Computations in Effects

<script lang="ts">
  let items = $state<Item[]>([]);
  
  // ❌ BAD: Heavy computation in effect
  $effect(() => {
    const sorted = [...items].sort((a, b) => {
      // Complex sorting logic
    });
    // Do something with sorted
  });
  
  // ✅ GOOD: Use $derived for computations
  let sortedItems = $derived.by(() => {
    return [...items].sort((a, b) => {
      // Complex sorting logic
    });
  });
  
  // Effect only for side effects
  $effect(() => {
    console.log('Sorted items updated:', sortedItems.length);
  });
</script>

Cleanup Long-Running Effects

<script lang="ts">
  let data = $state<Data | null>(null);
  
  $effect(() => {
    const controller = new AbortController();
    
    fetch('/api/data', { signal: controller.signal })
      .then(r => r.json())
      .then(d => data = d)
      .catch(e => {
        if (e.name !== 'AbortError') {
          console.error(e);
        }
      });
    
    // Cleanup: abort if component unmounts or effect re-runs
    return () => controller.abort();
  });
</script>

📦 Bundle Optimization

Code Splitting

// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [sveltekit()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Vendor chunks
          'vendor-svelte': ['svelte'],
          'vendor-chart': ['chart.js'],
        }
      }
    }
  }
});

Tree Shaking

// ✅ GOOD: Named imports enable tree shaking
import { formatDate, formatCurrency } from '$lib/utils';

// ❌ BAD: Imports entire module
import * as utils from '$lib/utils';

Analyze Bundle

# Install analyzer
npm install -D rollup-plugin-visualizer

# Add to vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    sveltekit(),
    visualizer({
      filename: 'stats.html',
      open: true
    })
  ]
});

📏 Measuring Performance

Browser DevTools

<script lang="ts">
  import { onMount } from 'svelte';
  
  onMount(() => {
    // Measure component mount time
    performance.mark('component-start');
    
    return () => {
      performance.mark('component-end');
      performance.measure('component-lifecycle', 'component-start', 'component-end');
    };
  });
</script>

Core Web Vitals

// src/lib/vitals.ts
import type { Metric } from 'web-vitals';

export function reportVitals(metric: Metric) {
  // Send to analytics
  console.log(metric.name, metric.value);
  
  // Or send to your analytics service
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify(metric)
  });
}

// In +layout.svelte
import { onMount } from 'svelte';
import { reportVitals } from '$lib/vitals';

onMount(async () => {
  const { onCLS, onFID, onFCP, onLCP, onTTFB } = await import('web-vitals');
  
  onCLS(reportVitals);
  onFID(reportVitals);
  onFCP(reportVitals);
  onLCP(reportVitals);
  onTTFB(reportVitals);
});

📋 Performance Checklist

Build Time

  • Minimal dependencies
  • Tree-shakeable imports
  • Code splitting configured
  • Image optimization

Runtime

  • Keys on {#each} blocks
  • Virtualization for long lists
  • Debounced inputs
  • Lazy loaded heavy components
  • Optimized images (lazy, responsive)

Data Loading

  • Parallel fetching where possible
  • Streaming for non-critical data
  • Prefetching for navigation
  • Caching strategies

Effects

  • Cleanup functions for subscriptions
  • Debounced network requests
  • Heavy computations in $derived
  • AbortController for fetch