Slow dashboard refreshes erode trust. This chapter compares three ways to sync state without page reloads.
Before you begin
- What you build: an
/activityscreen that loads once, keeps in sync via SSE or polling, and can grow into a WebSocket demo. - Why it matters: later chapters show multi-user views, so you need a reliable strategy and an escape hatch for higher interactivity.
- Watch out: reconnect when SSE or WebSockets drop, and avoid wasteful polling intervals that spike server costs.
Key terms
- Polling: the simplest strategy that re-fetches an API at fixed intervals.
- SSE (Server-Sent Events): a one-way text stream pushed from the server to the browser.
- WebSocket: a bidirectional channel where both parties can send and receive messages freely.
- Reconnect logic: timers or error handlers that retry when the network connection breaks.
Core ideas
Polling is easy to wire up but can send redundant requests. SSE streams data from server to client, so the UI updates as soon as the backend emits events. WebSockets bolster interactivity with bidirectional messaging. No matter the transport, implement retry logic for interrupted connections.
Realtime flow
👉 Mini recap: the initial load fetches once, then SSE pushes data or polling pulls it. If you need instantaneous bidirectional events, add WebSockets after SSE plus polling backups are stable.
Code examples
Start with polling and SSE, then add WebSockets as an optional upgrade.
// src/routes/(app)/activity/+page.svelte
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let data;
let tasks = structuredClone(data.tasks);
let interval: ReturnType<typeof setInterval> | null = null;
function apply(payload) {
tasks = payload;
}
onMount(() => {
const source = new EventSource('/api/tasks/stream');
source.onmessage = (event) => apply(JSON.parse(event.data));
source.onerror = () => {
source.close();
if (!interval) {
interval = setInterval(async () => {
const res = await fetch('/api/tasks');
apply(await res.json());
}, 5000);
}
};
return () => {
source.close();
if (interval) clearInterval(interval);
};
});
onDestroy(() => interval && clearInterval(interval));
</script>
<ActivityTimeline {tasks} />
<!-- src/routes/(app)/activity/+page.server.ts -->
export const load: PageServerLoad = async ({ fetch, locals }) => {
if (!locals.user) {
throw error(401, 'You must be signed in.');
}
const res = await fetch('/api/tasks');
if (!res.ok) {
throw error(res.status, 'Failed to load the initial data.');
}
return { tasks: await res.json() };
};
// src/routes/api/tasks/stream/+server.ts
export const GET = () => {
const stream = new ReadableStream({
start(controller) {
const timer = setInterval(async () => {
const payload = await getTasks();
controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`);
}, 3000);
return () => clearInterval(timer);
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
}
});
};
Optional: two-way WebSocket expansion
Skip this unless your team project truly needs it. SSE plus polling already cover most task notifications. When you do need a bidirectional channel, use a snippet like this:
const socket = io({ path: '/realtime' });
socket.on('task:sync', (task) => {
tasks = tasks.map((t) => (t.id === task.id ? task : t));
});
function toggle(task) {
socket.emit('task:update', { ...task, done: !task.done });
}
💬 댓글
이 글에 대한 의견을 남겨주세요