[Svelte Series 17] Real-time Updates with Polling, SSE, and WebSockets

한국어 버전

Slow dashboard refreshes erode trust. This chapter compares three ways to sync state without page reloads.

Before you begin

  • What you build: an /activity screen 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

  1. Polling: the simplest strategy that re-fetches an API at fixed intervals.
  2. SSE (Server-Sent Events): a one-way text stream pushed from the server to the browser.
  3. WebSocket: a bidirectional channel where both parties can send and receive messages freely.
  4. 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

+page.server load+page.sveltesetInterval fetch /api/tasksEventSource /api/tasks/streamsocket.io client/api/tasks/api/tasks/stream/realtime serverDB tasks table initial GETqueryJSONtasksdata.tasksoptional intervalGET /api/tasksEventSource connectsubscribeperiodic fetchpayloadpush dataemit task:updatesend updateupdate rowresultbroadcast task:sync

👉 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 });
}
http://localhost:5173/activity
DEV

UI Preview

Real-time timeline

Cards refresh immediately while the SSE link stays alive. If the stream drops, polling takes over seamlessly. With WebSockets enabled, toggling any task propagates to every open tab.

EventSourcepolling

SSE

/api/tasks/stream

Push payloads every 3 seconds.

Fallback

setInterval

Starts automatically when the stream errors.

WebSocket

task:update / task:sync

Bidirectional events.

👉 Mini recap: the first two tabs capture the required baseline (SSE plus fallback), and the WebSocket tab is an optional bonus. Skip the advanced tab if you need to finish the rest of the feature first.

Why it matters

Campus project trackers or campaign dashboards often have concurrent viewers. Picking the right real-time approach keeps data consistent. Polling is simple to deploy, SSE is perfect for server push without chat, and WebSockets provide the snappiest feedback at the cost of complexity.

Practice

  • Try it: choose polling or SSE to refresh /api/tasks on a schedule.
  • Extend it: wire up the WebSocket sample when you want a bidirectional demo.
  • Debug it: when connections drop, inspect clearInterval, EventSource.close(), and the socket reconnect logic first.
  • Definition of done: you can demo the live list in under five minutes and document the chosen strategy.

Wrap-up

Real-time updates make data trustworthy. Next, you will tighten performance with code splitting and loading strategies.

💬 댓글

이 글에 대한 의견을 남겨주세요