Documentation Index
Fetch the complete documentation index at: https://mintlify.com/sveltejs/svelte/llms.txt
Use this file to discover all available pages before exploring further.
Svelte is fast by default, but understanding performance optimization techniques helps you build even faster applications.
Reactivity Optimization
Use $state Sparingly
Only make variables reactive when necessary:
<script>
// ❌ Unnecessary reactivity
let apiUrl = $state('https://api.example.com');
let timeout = $state(5000);
// ✅ Good: These don't need to be reactive
const apiUrl = 'https://api.example.com';
const timeout = 5000;
// ✅ Only state that actually changes needs $state
let data = $state(null);
</script>
Reactive state has overhead. Use plain variables for constants and values that never change.
Choose Between $state and $state.raw
For large objects that are only reassigned (not mutated), use $state.raw:
<script>
// ❌ Expensive: Deep reactivity proxy for large object
let apiResponse = $state({
users: [...1000 users],
metadata: {...}
});
// ✅ Better: No deep proxying, just reassignment tracking
let apiResponse = $state.raw({
users: [...1000 users],
metadata: {...}
});
async function fetchData() {
// Reassigning works fine
apiResponse = await fetch('/api/data').then(r => r.json());
}
</script>
Use $state.raw when:
- Working with large API responses
- Data is replaced wholesale, not mutated
- Objects are frequently reassigned
Use $state when:
- Mutating nested properties (
user.name = 'Alice')
- Need fine-grained reactivity
- Working with small objects
Prefer $derived over $effect
Compute values with $derived, not $effect:
<script>
let count = $state(0);
// ❌ Bad: Using effect to compute derived value
let doubled;
$effect(() => {
doubled = count * 2;
});
// ✅ Good: Use $derived for computed values
let doubled = $derived(count * 2);
</script>
$derived is more efficient and clearer than $effect for computing values from state.
Avoid Reactive Statement Overhead
In legacy mode, reactive statements ($:) run more often than necessary:
<script>
let items = $state([...]);
// Svelte 4 style - runs on any dependency change
$: filteredItems = items.filter(item => item.active);
// ✅ Svelte 5 - more efficient
let filteredItems = $derived(items.filter(item => item.active));
</script>
Always use runes mode ($state, $derived, $effect) for better performance. Avoid legacy reactive statements.
Rendering Optimization
Use Keyed Each Blocks
Always provide keys in {#each} blocks for efficient updates:
<!-- ❌ Bad: Svelte re-renders all items -->
{#each items as item}
<div>{item.name}</div>
{/each}
<!-- ✅ Good: Svelte surgically updates only changed items -->
{#each items as item (item.id)}
<div>{item.name}</div>
{/each}
Performance impact:
- Without key: O(n) - Updates all existing DOM nodes
- With key: O(log n) - Moves/inserts/removes specific nodes
- Choose unique keys - Use
item.id, not array index
- Use primitives - Strings/numbers are better than objects
- Keep keys stable - Don’t recreate keys on each render
Avoid Index as Key
<script>
let items = $state(['A', 'B', 'C']);
function removeFirst() {
items = items.slice(1); // Now ['B', 'C']
}
</script>
<!-- ❌ Bad: Index changes when items are removed -->
{#each items as item, i (i)}
<div>{item}</div>
{/each}
<!-- After removal: 'B' gets index 0, 'C' gets index 1 -->
<!-- Svelte thinks these are NEW items -->
<!-- ✅ Good: Use stable identifier -->
{#each items as item (item)}
<div>{item}</div>
{/each}
Minimize Component Re-renders
Prevent unnecessary work by isolating reactive dependencies:
<!-- ❌ Bad: Entire component re-renders when count changes -->
<script>
let count = $state(0);
let expensiveData = $state([...large dataset]);
</script>
<button onclick={() => count++}>{count}</button>
<ExpensiveList items={expensiveData} />
<!-- ✅ Better: Isolate reactive scope -->
<script>
let count = $state(0);
let expensiveData = $state([...large dataset]);
</script>
<div>
<button onclick={() => count++}>{count}</button>
</div>
<ExpensiveList items={expensiveData} />
Component Design Patterns
Move expensive computations to dedicated components:
<!-- ❌ Bad: All logic in one component -->
<script>
let items = $state([...]);
let filter = $state('');
let processed = $derived(
items
.filter(item => item.name.includes(filter))
.map(item => expensiveTransform(item))
.sort((a, b) => a.score - b.score)
);
</script>
{#each processed as item (item.id)}
<div>{item.name}</div>
{/each}
<!-- ✅ Better: Separate concerns -->
<script>
import ItemList from './ItemList.svelte';
let items = $state([...]);
let filter = $state('');
</script>
<ItemList {items} {filter} />
Use Event Delegation
For many similar elements, use event delegation:
<!-- ❌ Bad: N event listeners -->
{#each items as item (item.id)}
<button onclick={() => handleClick(item.id)}>
{item.name}
</button>
{/each}
<!-- ✅ Better: One event listener -->
<div onclick={(e) => {
const id = e.target.dataset.id;
if (id) handleClick(id);
}}>
{#each items as item (item.id)}
<button data-id={item.id}>{item.name}</button>
{/each}
</div>
Avoid Inline Object/Array Creation
<!-- ❌ Bad: Creates new object every render -->
<Component style={{ color: 'red', fontSize: '16px' }} />
<!-- ✅ Good: Reuses same object -->
<script>
const buttonStyle = { color: 'red', fontSize: '16px' };
</script>
<Component style={buttonStyle} />
Bundle Size Optimization
Code Splitting with Dynamic Imports
Lazy load components that aren’t immediately needed:
<script>
let showModal = $state(false);
let Modal;
async function openModal() {
if (!Modal) {
const module = await import('./Modal.svelte');
Modal = module.default;
}
showModal = true;
}
</script>
<button onclick={openModal}>Open Modal</button>
{#if showModal && Modal}
<Modal />
{/if}
Tree Shaking
Import only what you need:
// ❌ Bad: Imports entire library
import _ from 'lodash';
const result = _.debounce(fn, 300);
// ✅ Good: Imports only debounce
import { debounce } from 'lodash-es';
const result = debounce(fn, 300);
Analyze Bundle Size
Use tools to find optimization opportunities:
# Install bundle analyzer
npm install -D rollup-plugin-visualizer
# Add to vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({ open: true })
]
};
Advanced Optimizations
Virtual Lists for Large Datasets
For thousands of items, render only visible rows:
<script>
import { VirtualList } from 'svelte-virtual-list';
let items = $state([...10000 items]);
</script>
<VirtualList items={items} let:item>
<div>{item.name}</div>
</VirtualList>
Debounce Expensive Operations
<script>
import { debounce } from './utils';
let searchTerm = $state('');
let results = $state([]);
const search = debounce(async (term) => {
results = await fetch(`/api/search?q=${term}`).then(r => r.json());
}, 300);
$effect(() => {
if (searchTerm) search(searchTerm);
});
</script>
<input bind:value={searchTerm} placeholder="Search..." />
Memoize Complex Computations
<script>
let data = $state([...]);
// Cache expensive computation
let cache = new Map();
let processed = $derived.by(() => {
const key = JSON.stringify(data);
if (cache.has(key)) return cache.get(key);
const result = expensiveComputation(data);
cache.set(key, result);
return result;
});
</script>
Image and Asset Optimization
Lazy Load Images
<img
src="placeholder.jpg"
data-src="high-res.jpg"
loading="lazy"
alt="Description"
/>
<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="Fallback" />
</picture>
Profiling and Measurement
- Open Chrome DevTools Performance tab
- Record interaction (click, scroll, etc.)
- Analyze flame graph for slow operations
- Look for long tasks and excessive re-renders
Use $inspect.trace
Debug reactive dependencies:
<script>
let items = $state([...]);
$effect(() => {
$inspect.trace('items effect'); // First line of effect
console.log('Items changed:', items.length);
});
// Logs what triggered this effect to re-run
</script>
Best Practices Checklist
- Use keyed
{#each} blocks for all lists
- Avoid
$state for constants - use plain variables
- Prefer
$derived over $effect for computed values
- Use
$state.raw for large objects that are only reassigned
- Lazy load large components with dynamic imports
- Debounce user input for expensive operations
- Profile before optimizing - measure, don’t guess
- Extract expensive logic to dedicated components
- Use virtual lists for >1000 items
- Optimize images with lazy loading and modern formats
Avoid these patterns:
- Using array index as
{#each} key
- Creating objects/arrays in templates:
style={{ color }}
- Making everything
$state “just in case”
- Using
$effect for derived values
- Not profiling before optimizing
- Premature optimization without measurements
Svelte is already very fast. Focus on correctness first, then optimize bottlenecks identified through profiling.