Skip to content
Angular ng ssr 4 min read

Event Replay

When Angular renders a page on the server, the HTML reaches the browser and becomes visible long before the JavaScript bundle has downloaded, parsed, and hydrated. During that window the page looks interactive, but no event listeners are attached yet — so a click or keystroke is simply lost. Event replay closes this gap by recording user interactions that happen before hydration and dispatching them again once the application is alive, so nothing the user did is dropped.

The pre-hydration gap

Server-side rendering produces a fast, meaningful first paint. The catch is that the visible DOM is inert until client-side bootstrap completes. The timeline looks like this:

  1. The server streams HTML; the browser paints it.
  2. The user, seeing a fully formed page, clicks a button or types into a field.
  3. The Angular bundle finishes loading and hydration runs, attaching real listeners.

Without event replay, any interaction in step 2 vanishes. The user perceives the page as broken or laggy, and on slower networks this gap can last several seconds. Event replay makes those early interactions feel as though the app was ready the whole time.

How event replay works

Angular’s hydration runtime installs a small, lightweight global listener at the document root very early — before the framework itself is bootstrapped. This listener uses event delegation to capture bubbling events (clicks, input, submit, and similar) into an in-memory queue, recording the target element and event details.

Once hydration completes and the real component listeners are wired up, Angular walks the queue and re-dispatches each captured event against the correct, now-hydrated element. From the component’s perspective, the handler simply fires as normal — it never knows the event was buffered.

The captured listener is intentionally tiny so it can be inlined and run before the main bundle. It only records events; it does not run your application logic until hydration has happened.

Enabling event replay

Event replay is an opt-in feature of Angular’s client hydration. Add withEventReplay() to provideClientHydration() in your application config.

import { ApplicationConfig } from '@angular/core';
import {
  provideClientHydration,
  withEventReplay,
} from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withEventReplay()),
  ],
};

That single feature flag is all that is required. Angular injects the early-capture script into the server-rendered document automatically and handles queueing and replay for you.

As of Angular 19, withEventReplay() is enabled by default in new SSR projects created with ng new --ssr. If you are upgrading an older app, add it explicitly.

A practical example

Consider a counter button shown immediately on the server. A user who is quick on the draw might click it before hydration.

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="increment()">Clicked {{ count() }} times</button>
  `,
})
export class CounterComponent {
  count = signal(0);

  increment(): void {
    this.count.update((n) => n + 1);
    console.log('handled click, count =', this.count());
  }
}

If the user clicks twice during the pre-hydration window, the clicks are queued. After hydration, both are replayed in order:

Output:

handled click, count = 1
handled click, count = 2

The component never sees a difference between a replayed click and a live one.

What gets replayed

Event replay focuses on the interaction events that users trigger directly. It does not attempt to replay everything in the DOM.

CategoryReplayed?Notes
click, dblclickYesThe most common pre-hydration interactions.
keydown, keyup, inputYesCaptures typing into forms before hydration.
submitYesEarly form submissions are preserved.
focus, blurNo (non-bubbling)Delegation relies on bubbling; these do not bubble.
mousemove, scrollNoHigh-frequency events are intentionally excluded.

Because capture relies on event delegation at the document, only bubbling events can be recorded. Non-bubbling events and continuous streams like scroll are out of scope.

Interaction with incremental hydration

Event replay pairs naturally with incremental hydration. With @defer (hydrate on interaction), an early click on a deferred block both triggers that block’s hydration and is then replayed against it once ready. This lets you ship minimal JavaScript up front while still honoring the very first interaction a user makes with any part of the page.

Best practices

  • Add withEventReplay() to provideClientHydration() for any SSR app where users may interact before the bundle loads.
  • Keep event handlers idempotent-friendly and order-aware; replayed events fire in the sequence they were captured.
  • Do not rely on focus, blur, scroll, or mousemove being replayed — design critical flows around bubbling events.
  • Avoid attaching your own raw document.addEventListener capture logic; let Angular own the pre-hydration listener to prevent double handling.
  • Measure your hydration timing (for example with web-vitals) so you know how large the pre-hydration window actually is for your users.
  • Combine event replay with incremental hydration to reduce initial JavaScript while still capturing the first interaction.
Last updated June 14, 2026
Was this helpful?