kciebiera

View on GitHub

Lecture 9

Event-Driven Pages and Event Streams

WWW 25/26 browser runtime · DOM · events · delays · scheduling · SSE


What This Lecture Is About

Lecture 8 introduced the browser’s programming model. This lecture focuses on what happens after the page is loaded.

We will focus on ideas:

The goal is not API memorisation. The goal is to understand how an interactive page lives over time.


Three Actors

User    -> clicks, types, scrolls, waits
Browser -> DOM, timers, rendering, event loop
Server  -> HTML, JSON, streamed events

An interactive page is a conversation between these three actors over time.


The Browser Is a Host Environment

JavaScript the language is only part of the story. In the browser, your program runs inside a host environment that provides extra objects:

Provided by browser Purpose
document access the DOM
window global browser context
console debugging
fetch HTTP requests
setTimeout timers
requestAnimationFrame schedule work before repaint
EventTarget event subscription model
EventSource SSE connection

In Node.js, many of these are different or historically absent.

Same language, different runtime.


The Browser Connection in One Diagram

HTML ──► DOM tree ──► JavaScript reads/writes DOM
                      ▲
                      │
                your program code
                      ▲
                      │
                 source files

So the real chain is:

  1. you write code
  2. tools prepare it for the browser
  3. the browser runs that JavaScript
  4. that code talks to browser APIs
  5. browser APIs connect your code to the page, the user, and the network

The Browser Is an Event Machine

After page load, the browser mostly waits.

wait for something to happen
    │
    ├─ user clicks
    ├─ user types
    ├─ timer fires
    ├─ fetch completes
    ├─ SSE message arrives
    └─ browser repaints

Your code does not run continuously like a physics simulation loop by default. It runs in small reaction steps when events happen.

This is the key mental shift from batch programming:


The Browser Is an Event Machine: Example

Example page lifecycle:

  1. page loads
  2. user types "dj"
  3. browser fires input
  4. code starts fetch("/api/posts/?search=dj")
  5. server replies later
  6. browser wakes your continuation
  7. UI updates

The important point is that steps 3 and 6 happen at different times.


Events Are the Unifying Abstraction

At first these look unrelated:

But conceptually they are the same:

Something happened outside your current function, so the runtime notifies you later.

That is what event-driven programming means.


Events Are the Unifying Abstraction

You describe:

button.addEventListener("click", () => console.log("clicked"));
input.addEventListener("input", () => console.log("typed"));
setTimeout(() => console.log("timer fired"), 1000);

Different APIs, same idea: register work now, run it later.


Event Sources Around the Page

User ───────────────► click / input / keydown
Browser ────────────► load / resize / visibilitychange
Timer system ───────► timeout / interval callback
HTTP stack ─────────► fetch promise resolves
Server push ────────► SSE message event

Different source, same shape:

source produces signal
        ▼
browser/runtime queues work
        ▼
your handler runs
        ▼
you update state / DOM

Event Sources Around the Page: Examples

Concrete examples:


Event Loop: The Traffic Controller

JavaScript in the page is usually single-threaded. The browser handles concurrency by scheduling work through the event loop.

external world -> browser APIs -> queue -> handler runs -> queue -> handler runs

This gives a powerful illusion:

That simplifies reasoning:

Responsiveness depends on handlers being short.


Event Loop: Blocking Example

Example:

button.addEventListener("click", () => {
  for (let i = 0; i < 1_000_000_000; i++) {}
  console.log("done");
});

While this loop runs, the page cannot respond smoothly to typing, clicks, or repainting.


Where Delays Come From

In browser programs, “later” has many causes:

Delay is normal in an interactive system, not an exception.


setTimeout(fn, 0) Still Means “Later”

console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");

Output:

A
C
B

The callback is queued and runs only after current work finishes.


Events Carry Data Across Boundaries

An event is not just “something happened.” It usually carries information from the outside world.

Examples:

So events are how the outside world injects new facts into your program.


Events Carry Data Across Boundaries: Example

input.addEventListener("input", event => {
  console.log(event.target.value);
});

The event is the carrier; the new text is the payload you care about.


One Click, Multiple Targets

One physical click can trigger logic at several levels of the DOM tree.

<div id="card">
  <button id="delete-btn">Delete</button>
</div>
card.addEventListener("click", () => console.log("card clicked"));
deleteBtn.addEventListener("click", () => console.log("button clicked"));
document.body.addEventListener("click", () => console.log("body clicked"));

If the user clicks the button, all three handlers may run.

Why?


One Click, Multiple Targets: Why It Matters

This is useful, but it can also surprise you.

Examples:

So one click may mean:

button action + parent action + global logging

This is why frontend code often checks:

A click is not just a point in space. It travels through the DOM as an event.


The Core UI Pattern

The browser gives you events. Your job is to turn them into state changes and visible output.

event -> interpret -> update state -> render UI

Examples:


The Core UI Pattern: Example

Mini example:

input.addEventListener("input", event => {
  state.query = event.target.value;
  render();
});

This is why events and state belong together conceptually.


Delays Create Ordering Problems

The hardest part of async code is often not waiting, but ordering.

User types:        d -------- dj
Requests sent:     A -------- B
Responses arrive:           B -------- A

If you blindly render every response, old data can overwrite new data.

Later request does not guarantee later response.


Debounce and Throttle

Delay is not always a bug. Sometimes delay is a tool.

**Debounce** - wait until activity stops - useful for search boxes - "send one request 300ms after typing stops"
**Throttle** - run at most once per interval - useful for scroll and resize - "update at most every 100ms"

Designing good interactive systems often means choosing which delays are useful.


A Useful Distinction: Pull vs. Push

How does the browser learn that the server has new data?

Two broad models:

**Pull** Client asks: ```text "Anything new?" ``` Examples: - page reload - `fetch()` - polling every 5 seconds
**Push** Server tells client: ```text "Here is new data." ``` Examples: - SSE - WebSockets - push notifications

This distinction matters more than any specific API.


Polling: Simulating Events by Repeated Questions

Polling means:

every N seconds:   ask server for updates

Conceptually, polling turns time into a fake event source:

timer fires -> fetch -> maybe new data

Why it is attractive:

Why it is unsatisfying:


Polling: Concrete Example

setInterval(async () => {
  const res = await fetch("/api/notifications");
  const data = await res.json();
  updateUI(data);
}, 5000);

Mental model:

timer fires -> fetch -> maybe new data

SSE: Turning Server Updates Into Browser Events

Server-Sent Events let the server keep an HTTP connection open and send messages over time.

From the browser’s point of view:

open connection once
        │
server sends message
        │
browser fires "message" event
        │
your handler runs

That is the key idea:

SSE makes server updates look like ordinary browser events.

This is why SSE feels conceptually elegant.


SSE vs. Traditional Fetch

traditional fetch:
client asks once -> server answers once

SSE:
client connects once -> server answers many times

Why SSE Fits the Browser So Well

SSE is a good conceptual match for many web applications because it preserves the browser’s existing model:

const source = new EventSource("/stream");
source.onmessage = event => {
  // treat incoming server data like any other event
};

Why SSE Fits the Browser So Well

Another example:

const source = new EventSource("/api/build-log");

source.addEventListener("message", event => {
  console.log("new log line:", event.data);
});

Mentally, this is close to:

button.addEventListener("click", ...)
input.addEventListener("input", ...)
source.addEventListener("message", ...)

SSE Is Not “Real-Time Magic”

SSE does not change the programming model. It only adds a new event source.

Before SSE:

user events + timers + fetch completions

After SSE:

user events + timers + fetch completions + server message events

So the important idea is not the API name. The important idea is that the browser can unify many asynchronous sources under one event-driven model.


Typical SSE Use Cases

SSE works best when the server mainly needs to announce updates.

Examples:

These all share one pattern:

server produces a sequence of facts
client displays them as they arrive

More concrete examples:

That is naturally a stream.


SSE vs. WebSockets

The conceptual difference is simple:

Technology Direction Best mental model
fetch() request -> response one question, one answer
Polling repeated request -> response repeated questions
SSE server -> client stream event feed
WebSocket both directions, ongoing conversation

If the client mostly listens, SSE is often enough. If both sides speak freely at any time, WebSockets fit better.

Do not choose the most powerful tool first. Choose the simplest model that matches the communication pattern.


The Deeper Pattern: Everything Becomes a Stream

A mature frontend eventually treats many things as streams of values over time:

value now, then later another value, then later another value

This is the bridge to ideas from RxJS, reactive UI, and functional reactive programming.

You do not need those frameworks to understand the core insight:

interactive programs are about data changing over time.


The Real Architectural Boundary

When building browser applications, there are really three layers:

outside world
  user / browser / server
        │
        ▼
events and data arrive
        │
        ▼
your program logic interprets them
        │
        ▼
state and DOM are updated

This suggests a clean design rule:

That rule works for clicks, forms, JSON, and SSE messages alike.


Common Misunderstandings

  1. setTimeout(fn, 0) runs immediately.” No. It only schedules work for later.

  2. “A click only belongs to one element.” No. Events can bubble through multiple levels of the DOM.

  3. “The newest request always returns last.” No. Network delays can reorder responses.

  4. “SSE is a completely different paradigm.” No. It is just another event source.


Design Heuristics


Summary


Questions?