# How the Summarize Chrome Extension Handles SPA Navigation and Triggers Summarization

> Discover how the Summarize Chrome extension handles SPA navigation using webNavigation.onHistoryStateUpdated and when summarization triggers with debouncing and delays.

- Repository: [Peter Steinberger/summarize](https://github.com/steipete/summarize)
- Tags: internals
- Published: 2026-02-19

---

**The extension detects SPA navigation via Chrome's `webNavigation.onHistoryStateUpdated` API, applies a 700ms debounce to prevent duplicate triggers, then calls `summarizeActiveTab(session, "spa-nav")` which inserts a 220ms rendering delay before extraction begins.**

The **steipete/summarize** Chrome extension automatically generates content summaries when users navigate Single-Page Applications (SPAs) that use the History API. By monitoring `pushState`, `replaceState`, and `popstate` events in the background script, the extension triggers summarization without requiring page reloads. This analysis examines the specific detection mechanisms and timing controls implemented in [`apps/chrome-extension/src/entrypoints/background.ts`](https://github.com/steipete/summarize/blob/main/apps/chrome-extension/src/entrypoints/background.ts).

## Detecting SPA Navigation with webNavigation.onHistoryStateUpdated

Modern SPAs update the browser URL without performing full page loads. The extension leverages the Chrome `webNavigation.onHistoryStateUpdated` event to capture these client-side transitions. This API specifically fires when JavaScript calls `history.pushState()` or `history.replaceState()`, or when the user triggers a `popstate` event.

In [`apps/chrome-extension/src/entrypoints/background.ts`](https://github.com/steipete/summarize/blob/main/apps/chrome-extension/src/entrypoints/background.ts) (lines 67‑79), the listener retrieves the tab information, validates the window session, and applies debouncing logic:

```typescript
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
  void (async () => {
    const tab = await chrome.tabs.get(details.tabId).catch(() => null);
    const windowId = tab?.windowId;
    if (typeof windowId !== "number") return;
    const session = getPanelSession(windowId);
    if (!session) return;
    const now = Date.now();
    if (now - session.lastNavAt < 700) return;   // debounce rapid SPA changes
    session.lastNavAt = now;
    void emitState(session, "");
    void summarizeActiveTab(session, "spa-nav");   // ← trigger summarization
  })();
});

```

The listener performs four critical operations: fetching the tab that generated the navigation event, retrieving the side-panel session via `getPanelSession(windowId)`, enforcing a 700ms debounce window to prevent rapid successive triggers, and finally invoking `summarizeActiveTab` with the `"spa-nav"` reason string.

## The Summarization Trigger Flow

Once the SPA navigation listener fires, control passes to `summarizeActiveTab`, which coordinates the extraction pipeline. This function handles multiple trigger types but applies specific timing logic for SPA navigation to ensure the DOM has stabilized.

### Debouncing Rapid Navigation Changes

The extension maintains a `lastNavAt` timestamp within each panel session. If successive `onHistoryStateUpdated` events occur within 700 milliseconds, the function returns early without triggering a new summarization. This prevents duplicate processing during rapid routing changes common in modern JavaScript frameworks like React Router or Vue Router.

### The 220ms Rendering Delay

After passing the debounce check, `summarizeActiveTab` recognizes the `"spa-nav"` reason and inserts a deliberate pause. In [`apps/chrome-extension/src/entrypoints/background.ts`](https://github.com/steipete/summarize/blob/main/apps/chrome-extension/src/entrypoints/background.ts) (lines 37‑40), the code awaits a timeout to allow the SPA's JavaScript to finish DOM mutations:

```typescript
if (reason === "spa-nav" || reason === "tab-url-change") {
  await new Promise((resolve) => setTimeout(resolve, 220));
}

```

This 220‑millisecond delay ensures that dynamic content has rendered before the extraction script runs. The same delay applies to `"tab-url-change"` events, which occur during traditional navigations where the URL changes but the page may still be loading content dynamically.

### Execution Pipeline

Following the delay, the function validates the current URL against blocked patterns, aborts any ongoing summarization requests using an `AbortController`, determines whether to use the fast-path extraction for YouTube URLs or full-page text extraction, and streams the results back to the side-panel UI via `emitState`.

## Other Summarization Triggers

While SPA navigation uses the `webNavigation` API, the extension monitors several other events through `chrome.tabs` listeners. Each trigger passes a distinct reason string to `summarizeActiveTab`, allowing the function to apply appropriate timing and validation logic.

**Tab Activation** (lines 82‑86): When users switch between tabs, `chrome.tabs.onActivated` fires, triggering summarization with the `"tab-activated"` reason.

```typescript
chrome.tabs.onActivated.addListener((info) => {
  const session = getPanelSession(info.windowId);
  if (!session) return;
  void emitState(session, "");
  void summarizeActiveTab(session, "tab-activated");
});

```

**URL Changes and Page Completion** (lines 89‑103): The `chrome.tabs.onUpdated` listener handles both URL changes (`changeInfo.url`) with reason `"tab-url-change"` and page load completion (`changeInfo.status === "complete"`) with reason `"tab-updated"`.

```typescript
chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
  const session = getPanelSession(tab.windowId);
  if (!session) return;
  if (typeof changeInfo.url === "string") {
    void summarizeActiveTab(session, "tab-url-change");
  }
  if (changeInfo.status === "complete") {
    void summarizeActiveTab(session, "tab-updated");
  }
});

```

The extension respects user preferences through an **auto-summarize** setting. Automatic triggers only execute when this setting is enabled, while manual actions (refresh buttons, length changes, explicit "Summarize" clicks) bypass this check using reasons like `"manual"`, `"refresh"`, or `"length-change"`.

## Summary

- **SPA Detection**: Uses `chrome.webNavigation.onHistoryStateUpdated` to capture History API calls (`pushState`, `replaceState`, `popstate`) without requiring full page reloads.
- **Debounce Protection**: Enforces a 700ms minimum interval between SPA navigation triggers to prevent duplicate processing during rapid routing.
- **Rendering Delay**: Applies a 220ms pause specifically for `"spa-nav"` and `"tab-url-change"` reasons to allow dynamic content to stabilize before extraction.
- **Multiple Triggers**: Supports automatic summarization via tab activation, URL updates, page completion events, and manual user actions, each with distinct reason strings for conditional logic.
- **Source Location**: All navigation listeners and the core `summarizeActiveTab` function reside in [`apps/chrome-extension/src/entrypoints/background.ts`](https://github.com/steipete/summarize/blob/main/apps/chrome-extension/src/entrypoints/background.ts).

## Frequently Asked Questions

### Why does the extension use a 700ms debounce for SPA navigation?

The 700ms debounce prevents redundant summarization calls when SPAs perform rapid successive navigation updates, such as during redirect chains or quick route transitions. By tracking `session.lastNavAt` and comparing against `Date.now()`, the extension ensures only the final stable state triggers a costly summarization operation.

### What is the purpose of the 220ms delay in summarizeActiveTab?

The 220ms delay allows the browser to complete JavaScript-driven DOM mutations that occur after the History API updates the URL. According to the source code in [`background.ts`](https://github.com/steipete/summarize/blob/main/background.ts), this pause ensures text extraction occurs after React, Vue, or other frameworks have finished rendering the new page content, preventing summaries of stale or partial data.

### How does SPA navigation detection differ from regular page load detection?

SPA detection uses `chrome.webNavigation.onHistoryStateUpdated`, which fires specifically for History API changes without network requests. Regular page loads trigger `chrome.tabs.onUpdated` with `status === "complete"` or URL change events. The extension distinguishes these via reason strings (`"spa-nav"` vs `"tab-updated"`) and applies the 220ms delay only to the former two cases where JavaScript rendering likely continues after the event fires.

### Can users disable automatic summarization on SPA navigation?

Yes. The extension checks an auto-summarize user preference before executing automatic triggers. While the background script sets up the `onHistoryStateUpdated` listener regardless, the `summarizeActiveTab` function respects user settings and only proceeds automatically when enabled. Manual summarization via the side-panel UI remains available regardless of this setting.