DEV Community

Cover image for Real-world Svelte 5: Handling high-frequency real-time data with Runes
Polliog
Polliog

Posted on

Real-world Svelte 5: Handling high-frequency real-time data with Runes

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;
            }
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    })
);
Enter fullscreen mode Exit fullscreen mode

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)