Discarding and Muting Tabs to Save Memory

Free memory and silence noise from an MV3 extension: chrome.tabs.discard, autoDiscardable, idle-tab policies on alarms, protecting audible, pinned and form-filled tabs, muting with mutedInfo, and Firefox differences.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

Users with dozens of open tabs install tab-suspender extensions to get memory back, and noise-control extensions to stop autoplaying videos in background tabs. MV2 suspenders replaced pages with a placeholder of their own; MV3 provides the browser’s native mechanism directly: chrome.tabs.discard unloads a tab’s page from memory while keeping it in the tab strip, to be reloaded when the user returns. Muting is a single tabs.update. Both are easy to call and easy to misuse — discarding the tab with an unsaved form, the one playing music, or the one the user is about to click. This guide builds a careful policy. It belongs to tabs API and window management.

What discarding does

A discarded tab keeps its title, favicon, position and session history, but its renderer process — the page, its scripts, its memory — is torn down. When the user activates it, the browser reloads the page from its URL (with back/forward cache where possible), so scroll position and form contents may or may not survive depending on the site. chrome.tabs.discard(tabId) discards one tab and resolves to the discarded tab object, which has a new tab id in some versions — a detail that breaks code holding on to the old id. The browser also discards tabs on its own under memory pressure; tab.autoDiscardable = false opts a tab out of that automatic discarding. The active tab of a focused window cannot be discarded.

Tab state before and after discardingWhat a discarded tab keeps — title, favicon, position, session history — and loses — page memory, running scripts, unsaved form state, playing media.AspectKeptLostTitle, favicon, positionYes—Back/forward historyYes—Page memory, scripts—FreedUnsaved form inputSometimesOftenPlaying audio/video—StoppedTab idUsuallyMay change
Discarding is cheap to undo for reading; expensive for anything in progress.

Step-by-step: a respectful discard policy

1. Declare permissions

1{ "permissions": ["tabs", "alarms", "storage"] }

Execution context: the manifest. tabs.discard and tabs.update work with no special permission, but reading url, audible, and lastAccessed to decide which tabs to discard needs tabs. Alarms drive the periodic policy check.

2. Run the policy on an alarm

1// sw.js
2chrome.alarms.create("discard-policy", { periodInMinutes: 5 });
3chrome.alarms.onAlarm.addListener(async ({ name }) => {
4  if (name !== "discard-policy") return;
5  const { discardAfterMin = 30, enabled = true } = await chrome.storage.sync.get(["discardAfterMin", "enabled"]);
6  if (enabled) await discardIdleTabs(discardAfterMin * 60_000);
7});

Execution context: the service worker, listener at the top level. A five-minute check is frequent enough to reclaim memory and rare enough to cost nothing. User settings — the idle threshold and an on/off switch — live in chrome.storage.sync so they follow the user.

3. Choose candidates carefully

 1async function discardIdleTabs(idleMs) {
 2  const now = Date.now();
 3  const { protectedHosts = [] } = await chrome.storage.sync.get("protectedHosts");
 4  const tabs = await chrome.tabs.query({ discarded: false, active: false });
 5  const candidates = tabs.filter((t) =>
 6    !t.pinned && !t.audible && t.autoDiscardable !== false &&
 7    t.status === "complete" && t.url?.startsWith("http") &&
 8    !protectedHosts.some((h) => new URL(t.url).hostname.endsWith(h)) &&
 9    now - (t.lastAccessed ?? now) > idleMs);
10  for (const t of candidates) {
11    if (await hasUnsavedInput(t.id)) continue;
12    await chrome.tabs.discard(t.id).catch(() => {});
13  }
14}

Execution context: the service worker. Pinned tabs, audible tabs, tabs the user marked as not auto-discardable, and hosts the user protected are never touched. lastAccessed (Chrome 121+) measures how long since the user looked at the tab; without it, track activation times yourself with tabs.onActivated. Only loaded http(s) pages are candidates — discarding a page still loading or an extension page gains little.

Deciding whether to discard a tabEvery five minutes the worker queries inactive, loaded tabs, removes pinned, audible, protected and recently used ones, checks for unsaved input via a content script, and discards the rest.Alarmevery 5 mintabs.queryinactive, not discardedFilterpinned, audible, protectedidle longer than thresholdUnsaved input?ask content scripttabs.discardfree memoryBadge counttabs suspended
Each filter removes a way discarding could cost the user something.

4. Check for unsaved input before discarding

 1async function hasUnsavedInput(tabId) {
 2  try {
 3    const [{ result }] = await chrome.scripting.executeScript({
 4      target: { tabId },
 5      func: () => [...document.querySelectorAll("textarea, input:not([type=hidden]):not([type=checkbox]):not([type=radio]), [contenteditable=true]")]
 6        .some((el) => (el.value ?? el.textContent ?? "").trim().length > 0 && el.matches(":focus-within, :not(:placeholder-shown)")),
 7    });
 8    return result;
 9  } catch {
10    return true;                               // cannot check → assume yes, don't discard
11  }
12}

Execution context: the service worker injecting into the candidate tab, which needs host access (or <all_urls>) and scripting. A half-written email or comment is the most painful thing a suspender can lose. The heuristic is deliberately cautious: any non-empty text field blocks discarding, and failure to check counts as “unsaved”. Pages that use beforeunload warnings are another signal you can’t read directly — err on the side of keeping tabs.

5. Let users protect tabs explicitly

1export async function setTabProtected(tabId, on) {
2  await chrome.tabs.update(tabId, { autoDiscardable: !on });
3  const { protectedTabs = [] } = await chrome.storage.session.get("protectedTabs");
4  await chrome.storage.session.set({ protectedTabs: on ? [...new Set([...protectedTabs, tabId])] : protectedTabs.filter((id) => id !== tabId) });
5}

Execution context: the service worker, triggered from the popup or a context menu on the action. Setting autoDiscardable: false also protects the tab from the browser’s own memory-pressure discarding — a useful side effect for a tab the user cares about. Offer “Never suspend this site” as well, stored in protectedHosts.

A tab is discarded and restoredThe policy discards an idle tab; the badge shows the number of suspended tabs; the user clicks the tab; the browser reloads the page from its URL; the extension's content script re-runs.Service workerBrowserUsertabs.discard(id)renderer releasedbadge: 14 suspendedclicks tabreload from URLtabs.onUpdated (loading → complete)
Restoration is the browser's job — the page reloads when the user returns.

6. Mute noisy background tabs

 1chrome.tabs.onUpdated.addListener(async (tabId, change, tab) => {
 2  if (change.audible !== true) return;
 3  const { autoMuteBackground } = await chrome.storage.sync.get("autoMuteBackground");
 4  if (autoMuteBackground && !tab.active) {
 5    await chrome.tabs.update(tabId, { muted: true });
 6  }
 7});
 8
 9chrome.tabs.onActivated.addListener(async ({ tabId }) => {
10  const tab = await chrome.tabs.get(tabId);
11  if (tab.mutedInfo?.muted && tab.mutedInfo.reason === "extension" && tab.mutedInfo.extensionId === chrome.runtime.id) {
12    await chrome.tabs.update(tabId, { muted: false });          // unmute what we muted when the user looks
13  }
14});

Execution context: the service worker. change.audible reports when a tab starts producing sound. mutedInfo records who muted the tab — the user, a capture, or which extension — so you only unmute tabs you muted yourself, never overriding the user’s own choice.

7. Report what you did

A badge count of suspended tabs, and a popup list with “Restore all” and per-tab “Restore”, make the extension’s effect visible. Users who cannot tell why their tabs keep reloading will blame the browser — or uninstall.

Common mistakes

  • Discarding audible or pinned tabs. Music stops; pinned apps reload.
  • Ignoring unsaved input. Lost drafts are the most common complaint about suspenders.
  • Holding on to old tab ids. Re-query after discarding; ids can change.
  • Unmuting tabs the user muted. Check mutedInfo.reason and extensionId.
  • Discarding too aggressively. Reloads cost network and time; thirty minutes is a sensible default.

Cross-browser variation

  • Chrome / Edge: tabs.discard, autoDiscardable, lastAccessed (121+), and mutedInfo with reasons.
  • Firefox: supports tabs.discard and lastAccessed; the discarded tab keeps its id; autoDiscardable is supported in recent versions. Firefox may not allow discarding some special pages.
  • Safari: no tabs.discard; muting support is limited. Memory-saving features cannot be implemented on Safari.

Verification

  1. Set the threshold to one minute in a test profile, leave several tabs idle, and confirm they become discarded (chrome://discards shows the state).
  2. Type into a textarea in a background tab and confirm it is not discarded.
  3. Play audio in a background tab with auto-mute on and confirm it mutes, then unmutes when activated.
  4. Protect a tab and confirm neither your policy nor chrome://discards’ “urgent discard” removes it.

FAQ

Does discarding reduce memory immediately?

Usually within seconds, as the renderer process exits. Tabs sharing a process with others free less.

Will the page remember its scroll position?

Often, through the back/forward cache or the site’s own logic, but not reliably. Do not promise it.

Can I discard tabs in other windows?

Yes, any non-active tab in any window, subject to the same filters.

How do I restore all suspended tabs at once?

Reloading a discarded tab with chrome.tabs.reload(tabId) brings it back without activating it. Loop over tabs.query({ discarded: true }) with a small concurrency limit — restoring fifty tabs simultaneously recreates the memory spike the feature existed to avoid.

Does discarding interfere with my own content scripts?

They are torn down with the page and re-injected when it reloads, like any navigation. Any in-page state they held is lost, so keep it in storage if it matters.

Is there a way to know the browser discarded a tab, not my extension?

tabs.onUpdated reports discarded: true in the change info either way. If you need to distinguish, record the ids you discard yourself just before calling tabs.discard.

Other Core APIs & Cross-Browser Data Management Resources