Vivian Voss

Native WebSocket: Server Pushes, Client Listens

javascript web architecture

Stack Patterns ■ Episode 07

Your frontend needs live data. A counter. A feed. A notification badge. The server knows when something changes. The client does not.

React taught an entire generation of developers, misleadingly, to poll.

useEffect(() => {
  const id = setInterval(() => fetch('/api/count')
    .then(r => r.json()).then(setCount), 3000);
  return () => clearInterval(id);
}, []);

A hook. A cleanup. A state setter. An interval. Every three seconds: "Anything new?" No. "Anything new?" No. "Anything new?" "Yes, forty seconds ago."

Splendid timing.

The React ecosystem's answer: npm install socket.io-client. 46 KB. One install averaging 1,400 transitive dependencies from a registry whose greatest contributions to the industry include left-pad and event-stream. One does hope yours are safe. The browser solved this in 2011 with precisely zero.

One would think someone might have mentioned that.

Polling vs WebSocket setInterval + fetch (every 3s) client server new? → no new? → no new? → no new? → yes (40s late) WebSocket (persistent connection) client server HTTP upgrade (once) push when data changes Polling: N requests/min N−1 return nothing WebSocket: 1 connection 2 bytes per frame

The Client

const ws = new WebSocket('ws://localhost:3000/ws');
ws.onmessage = e =>
  document.querySelector('#count').textContent = e.data;

Two lines. No npm. No useEffect. No cleanup. Native to every browser since 2011. React arrived two years later and never mentioned it. Rather an oversight for a 136 KB framework, that.

The Server

One function. One purpose. Push when state changes.

Rust (axum, two crate dependencies):

async fn push(mut ws: WebSocket) {
    let mut n = 0u64;
    loop {
        n += 1;
        if ws.send(Message::Text(n.to_string()))
            .await.is_err() { break }
        sleep(Duration::from_secs(1)).await;
    }
}

Go (gorilla/websocket):

func push(conn *websocket.Conn) {
    n := 0
    for {
        n++
        conn.WriteMessage(websocket.TextMessage,
            []byte(strconv.Itoa(n)))
        time.Sleep(time.Second)
    }
}

Same client. Same protocol. One function does one thing. No npm on the server either.

PHP requires Swoole or ReactPHP. Persistent connections against a share-nothing architecture. Rather like fitting a sail to a submarine.

Why It Works

WebSocket is a persistent, full-duplex TCP channel. RFC 6455 defines the protocol: HTTP upgrades the connection once, then both sides send at will. Overhead per frame: two bytes. No library required on either side.

The WebSocket Handshake Browser Server GET /ws HTTP/1.1 Upgrade: websocket 101 Switching Protocols persistent TCP (full-duplex) 2 bytes per frame

What This Replaces

useEffect + setInterval + fetch: React's answer to real-time. N requests per minute, N minus 1 return nothing.

Socket.IO: 46 KB client plus npm dependency chain. Marvellous for 10,000 concurrent chat users. Rather generous for a live counter.

Long polling: a 2006 workaround that somehow still has a job.

What Native WebSocket Replaces useEffect + setInterval + fetch Socket.IO (46 KB + npm chain) long polling (2006 workaround) new WebSocket(url) native since 2011, 0 dependencies, 2 bytes/frame

When to Use

Dashboards, notifications, tickers, build logs: anything where the server knows before the client does. When not: multiplayer games, collaborative editors. There, the frameworks earn their kilobytes.

The Point

Two lines of JavaScript. One function on the server. Zero npm packages. The browser has been ready since 2011.

Whenever you are.