0b76a4b119
- 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
13 KiB
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