DEV Community

Cover image for Why Chrome tabs eat memory, and how we built real-time duplicate prevention in Manifest V3
Latr
Latr

Posted on Originally published at latr.page

Why Chrome tabs eat memory, and how we built real-time duplicate prevention in Manifest V3

Handling duplicate tabs, URL normalization, automated grouping rules, and memory management in a local-first Chrome extension.

If you keep your browser open across a working day, you almost certainly accumulate duplicate tabs.

You open a pull request in the morning. At 2 PM someone pings a link to that same PR in Slack, and you click it. At 4 PM you click an email notification about a review comment on that same PR. Now you have three identical tabs loaded into RAM, spread across different windows.

Chrome and Edge provide a native tab search shortcut (Ctrl+Shift+A), but no built-in way to automatically detect, deduplicate, or prevent duplicate tabs.

When we built Latr, our local-first browser workstation, duplicate handling became one of our core features.

It sounds deceptively simple on paper: just match URLs and close the second one. In practice, building automated duplicate detection and tab management in a production browser extension brings up several tricky engineering problems.

Here is what we learned building it under Manifest V3.

The Normalization Problem: When Are Two URLs Actually the Same?

A naive duplicate check uses strict string equality (tabA.url === tabB.url). In the real world, that fails more often than it succeeds.

Consider these four URLs:

  1. https://github.com/facebook/react/pull/1234
  2. https://github.com/facebook/react/pull/1234/ (trailing slash)
  3. https://github.com/facebook/react/pull/1234?utm_source=slack&utm_medium=chat (marketing parameters)
  4. https://github.com/facebook/react/pull/1234#issuecomment-5678 (comment anchor)

If a link arrives from Slack, Twitter, or an email, it carries tracking parameters like utm_source, utm_campaign, fbclid, or gclid. The user is looking at the exact same document, but the URL strings differ.

Here is how we normalize URLs before running the duplicate detection:

const TRACKING_PARAMS = new Set([
  'utm_source',
  'utm_medium',
  'utm_campaign',
  'utm_term',
  'utm_content',
  'fbclid',
  'gclid',
  'ref',
  'source',
]);

export function normalizeUrl(rawUrl, { ignoreHash = false } = {}) {
  try {
    const parsed = new URL(rawUrl);

    // Normalize protocol and hostname
    parsed.protocol = parsed.protocol.toLowerCase();
    parsed.hostname = parsed.hostname.toLowerCase();

    // Drop trailing slash on path
    if (parsed.pathname.length > 1 && parsed.pathname.endsWith('/')) {
      parsed.pathname = parsed.pathname.slice(0, -1);
    }

    // Strip known tracking parameters
    for (const key of Array.from(parsed.searchParams.keys())) {
      if (TRACKING_PARAMS.has(key.toLowerCase())) {
        parsed.searchParams.delete(key);
      }
    }

    // Sort remaining query params so order does not matter
    parsed.searchParams.sort();

    // Optionally ignore fragment identifier
    if (ignoreHash) {
      parsed.hash = '';
    }

    return parsed.toString();
  } catch {
    return rawUrl;
  }
}
Enter fullscreen mode Exit fullscreen mode

By stripping known tracking tokens and sorting the remaining query parameters, links clicked from notifications match tabs that were opened manually.

Guard Rails: What You Must Never Automatically Close

Closing a duplicate cannot be an aggressive, indiscriminate script. In browser extension development, destroying a user's uncommitted work is the worst bug you can ship.

Before flagging or closing any tab as a duplicate, our duplicate filter enforces four safety rules:

  1. Never close pinned tabs: A pinned tab is an explicit declaration of intent. It stays.
  2. Never close audible tabs: If a duplicate tab is currently playing audio (such as a meeting, podcast, or YouTube video), touching it is jarring and unexpected.
  3. Never close active tabs: If the user is currently looking at a tab, that tab is exempt. The older background copy is the candidate for closing, never the active viewport.
  4. Never touch system or local pages: Internal URLs (chrome://, about:, file://, extension setup pages) are excluded outright.

Here is how that filtering looks in practice:

export function duplicateTabs(tabs = [], { loose = false } = {}) {
  const seen = new Map();
  const duplicates = [];

  for (const tab of tabs) {
    // Safety checks
    if (!tab.url || !/^https?:/i.test(tab.url)) continue;
    if (tab.pinned) continue;
    if (tab.audible) continue;

    const key = normalizeUrl(tab.url, { ignoreHash: loose });

    if (seen.has(key)) {
      // The earlier tab survives; this later copy is marked as duplicate
      duplicates.push(tab);
    } else {
      seen.set(key, tab);
    }
  }

  return duplicates;
}
Enter fullscreen mode Exit fullscreen mode

Real-Time Prevention: The onCreated vs onUpdated Race Condition

Finding duplicates on demand inside an extension popup is straightforward. Doing it in real time as links open requires handling browser lifecycle timing.

When an external program (like Slack or an IDE) opens a URL:

  1. chrome.tabs.onCreated fires, but tab.url is often empty or about:blank while the initial process allocates.
  2. chrome.tabs.onUpdated fires multiple times as the page navigates (status: 'loading'), receives headers, and completes (status: 'complete').

If duplicate prevention is turned on, running the check on onCreated misses the incoming URL. Running it blindly on every onUpdated event causes redundant queries across every window.

The solution is to hook onUpdated, listen specifically for the first time changeInfo.url appears, verify if an existing tab in any open window shares the normalized URL, focus that existing tab, and silently close the new duplicate before it consumes full render memory.

chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
  if (!changeInfo.url) return;
  if (!settings.preventDuplicates) return;

  const targetKey = normalizeUrl(changeInfo.url);
  const allTabs = await chrome.tabs.query({});

  const match = allTabs.find((existing) => 
    existing.id !== tabId && 
    !existing.pinned && 
    normalizeUrl(existing.url) === targetKey
  );

  if (match) {
    // Jump to the tab the user already had open
    await chrome.tabs.update(match.id, { active: true });
    await chrome.windows.update(match.windowId, { focused: true });

    // Close the redundant incoming tab
    await chrome.tabs.remove(tabId);
  }
});
Enter fullscreen mode Exit fullscreen mode

The Memory Angle: Discarding Inactive Tabs

Closing duplicates helps, but what about the 30 tabs you actually want to keep open?

Modern browsers consume hundreds of megabytes per open tab because each tab maintains its own isolated process, DOM tree, and JavaScript runtime.

Instead of closing tabs, Manifest V3 extensions can leverage chrome.tabs.discard(tabId).

Calling discard() instructs the browser engine to dump the tab's renderer process and memory footprint while keeping the tab visible in the tab strip. The title, favicon, and URL remain completely intact. When the user eventually clicks back to that tab, the browser restores it from cache.

We combined this with our duplicate cleanup: users can see their total open tab count, close real duplicates in one click, and put idle tabs to sleep to immediately recover RAM without losing their layout.

Final Thoughts

Tab management tools often fail when they try to become complex workspaces that force users to adopt an entirely new system.

By focusing on small, high-leverage problems (stripping tracking parameters, intercepting redundant links, and discarding cold tabs), you can solve the tab clutter problem natively inside the browser.

If you want to try out duplicate prevention and tab snoozing in a lightweight, local-first utility, give Latr a look. It requires zero install-time permission warnings and keeps all data on your machine.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.