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
652 lines
13 KiB
Markdown
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
|