Chrome Extension Extension Cleanup Patterns — Best Practices

5 min read

Cleanup and Teardown Patterns for Chrome Extensions

Proper cleanup ensures extensions leave no trace when disabled, uninstalled, or updated. This document covers essential patterns for graceful shutdown.

Uninstall URL

Set an exit survey URL when users uninstall your extension:

// background.js
chrome.runtime.setUninstallURL('https://yourdomain.com/uninstall-survey');

Content Script Cleanup

Remove injected elements, event listeners, and observers when content scripts unload:

// content-script.js
class ContentScriptCleanup {
  constructor() {
    this.elements = [];
    this.listeners = [];
    this.observers = [];
  }

  addElement(el) { this.elements.push(el); }
  addListener(fn, target = window) { 
    this.listeners.push({ fn, target }); 
  }
  addObserver(observer) { this.observers.push(observer); }

  cleanup() {
    // Remove injected DOM elements
    this.elements.forEach(el => el.remove());
    this.elements = [];

    // Remove event listeners
    this.listeners.forEach(({ fn, target }) => {
      target.removeEventListener('click', fn);
    });
    this.listeners = [];

    // Disconnect MutationObservers
    this.observers.forEach(obs => obs.disconnect());
    this.observers = [];
  }
}

const cleanup = new ContentScriptCleanup();

// Example: injected element
const widget = document.createElement('div');
document.body.appendChild(widget);
cleanup.addElement(widget);

// Example: observer
const observer = new MutationObserver(() => {});
observer.observe(document.body, { childList: true });
cleanup.addObserver(observer);

// Cleanup on unload
window.addEventListener('unload', () => cleanup.cleanup());

Service Worker State Preservation

Save state before the service worker terminates:

// background.js
chrome.runtime.onSuspend.addListener(() => {
  // Save current state to storage
  chrome.storage.local.set({
    lastState: getCurrentState(),
    timestamp: Date.now()
  });
});

Port Disconnect Handling

Clean up when communication ports are disconnected:

// background.js
const ports = new Set();

chrome.runtime.onConnect.addListener(port => {
  ports.add(port);
  port.onDisconnect.addListener(() => {
    ports.delete(port);
    // Perform cleanup for this port's context
  });
});

Alarm Cleanup

Clear all alarms when a feature is disabled:

// background.js
function disableFeature(featureId) {
  chrome.alarms.clearAll();
  // Disable feature logic
}

Context Menu Cleanup

Remove all context menu items:

// background.js
chrome.contextMenus.removeAll(() => {
  console.log('Context menus cleared');
});

Storage Migration Cleanup

Remove obsolete keys during version upgrades:

// background.js
chrome.runtime.onInstalled.addListener(details => {
  if (details.reason === 'update') {
    chrome.storage.local.get(['oldKey1', 'oldKey2'], items => {
      if (items.oldKey1 !== undefined) {
        chrome.storage.local.remove(['oldKey1', 'oldKey2']);
      }
    });
  }
});

Tab Cleanup

Close extension-opened tabs on uninstall:

// background.js
chrome.runtime.onUninstalled.addListener(() => {
  chrome.tabs.query({ url: '*://your-extension-tabs.com/*' }, tabs => {
    tabs.forEach(tab => chrome.tabs.remove(tab.id));
  });
});

CSS Cleanup

Remove dynamically injected styles:

// content-script.js
chrome.runtime.onMessage.addListener(msg => {
  if (msg.action === 'cleanup') {
    chrome.scripting.removeCSS({
      css: '/* injected styles */',
      target: { tabId: chrome.tab.id }
    });
  }
});

Memory Leak Prevention

Nullify references and clear intervals:

// Always clear intervals and nullify references
let intervalId = setInterval(doWork, 1000);

// On cleanup
clearInterval(intervalId);
intervalId = null;
someObject.reference = null;

Comprehensive Cleanup Manager

// cleanup-manager.js
class ExtensionCleanupManager {
  constructor() {
    this.cleanupTasks = [];
  }

  register(task) {
    this.cleanupTasks.push(task);
  }

  async executeAll() {
    for (const task of this.cleanupTasks) {
      try {
        await task();
      } catch (e) {
        console.error('Cleanup failed:', e);
      }
    }
    this.cleanupTasks = [];
  }
}

const manager = new ExtensionCleanupManager();

// Register cleanup tasks
manager.register(() => chrome.alarms.clearAll());
manager.register(() => chrome.contextMenus.removeAll());
manager.register(() => chrome.notifications.clear());

// On disable/uninstall
manager.executeAll();

See Also

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.