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

652 lines
13 KiB
Markdown

# 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
```svelte
<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
```svelte
<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
```svelte
<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
```svelte
<!-- ❌ 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
```svelte
<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
```svelte
<!-- 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:
```svelte
<!-- 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
```typescript
// 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
```typescript
// 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())
};
};
```
```svelte
<!-- +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
```svelte
<!-- 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
```svelte
<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
```svelte
<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
```svelte
<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
```svelte
<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
```svelte
<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
```typescript
// 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
```typescript
// ✅ GOOD: Named imports enable tree shaking
import { formatDate, formatCurrency } from '$lib/utils';
// ❌ BAD: Imports entire module
import * as utils from '$lib/utils';
```
### Analyze Bundle
```bash
# 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
```svelte
<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
```typescript
// 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