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.