How the Summarize Chrome Extension Handles SPA Navigation and Triggers Summarization

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.

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 (lines 67‑79), the listener retrieves the tab information, validates the window session, and applies debouncing logic:

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 (lines 37‑40), the code awaits a timeout to allow the SPA's JavaScript to finish DOM mutations:

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.

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".

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.

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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →