Svelte 5 is officially out, and it introduces Runes: a completely new way to handle reactivity.
Most tutorials show you how to build a "Counter" app with $state. That's cool, but I wanted to see how it performs in the trenches.
So, I built LogWard entirely with SvelteKit 5. It's a log management dashboard (like Datadog) that streams logs in real-time via Server-Sent Events (SSE).
Here's what I learned about moving from Stores to Runes in a data-heavy application.
The Challenge: The "Live Tail"
Imagine a WebSocket or SSE connection pumping 50-100 log lines per second into your frontend. In Svelte 4, you would likely use a writable store and subscribe to it.
The problem? Array mutations and large object updates can trigger unnecessary re-renders of parent components if you aren't careful.
The Solution: $state and Fine-Grained Reactivity
With Runes, reactivity is opt-in and fine-grained. Here's how I structured the log ingestion.
1. The Log Store (Simplified)
Instead of a store, I just use a class or a function that returns a state object.
// logs.svelte.ts
export class LogStream {
// Look how clean this is. No 'writable', just $state.
logs = $state([]);
isStreaming = $state(false);
constructor() {
this.connect();
}
connect() {
this.isStreaming = true;
const eventSource = new EventSource('/api/v1/stream');
eventSource.onmessage = (event) => {
const newLogs = JSON.parse(event.data);
// Direct mutation! No need for: logs.update(l => [...l, ...new])
// Svelte 5 proxies the array and updates only the DOM nodes that care.
this.logs.unshift(...newLogs);
// Keep array size manageable
if (this.logs.length > 1000) {
this.logs.length = 1000;
}
};
}
}
2. Derived State for Filtering
This is where Runes shine. In a log dashboard, you filter by level (Info, Error) or service. Previously, derived stores could be clunky to chain. Now, it's just $derived.
// filtering.svelte.ts
let searchQuery = $state('');
let levelFilter = $state('ALL');
// This re-calculates ONLY when searchQuery or levelFilter changes.
// It does NOT re-calculate if 'someOtherState' changes.
const filteredLogs = $derived(
allLogs.filter(log => {
const matchesLevel = levelFilter === 'ALL' || log.level === levelFilter;
const matchesQuery = log.message.includes(searchQuery);
return matchesLevel && matchesQuery;
})
);
3. UI Performance with shadcn-svelte
I used the shadcn-svelte port for the UI. Since shadcn components are headless/accessible, combining them with Svelte 5 was mostly seamless, although some older libraries still rely on slot syntax (which is deprecated in favor of snippets, but still supported).
The result is a dashboard that feels native. I can have the "Live Tail" open receiving 100 logs/sec, and the UI remains 60fps responsive because Svelte is surgically updating text nodes, not re-rendering the whole data table component tree.
Verdict: Is Svelte 5 Ready?
For my use case: Yes. The code is cleaner. I deleted so much boilerplate code (subscribing/unsubscribing to stores). The mental model of "It's just JavaScript variables" makes handling complex state logic much easier.
If you want to see the full source code (it's a monorepo with Fastify backend), check it out on GitHub. I'd love feedback on my Runes implementation!
📦 Source Code: https://github.com/logward-dev/logward
🚀 Live App: https://logward.dev
Top comments (0)