Chrome Extension Throttle Debounce Extensions — Best Practices

6 min read

Throttle and Debounce Patterns for Chrome Extensions

Chrome extensions face unique performance challenges that require throttle and debounce patterns. Storage writes, API calls, DOM mutations, and message passing can overwhelm the extension if left uncontrolled. This guide covers implementations optimized for extension contexts, especially service workers.

Why Throttle and Debounce Matter in Extensions

Unlike regular web apps, extensions run in multiple contexts (popup, background, content scripts) with independent lifecycles. Uncontrolled operations can cause:

Debounce Patterns

Debounce delays execution until after a quiet period. Use when: user stops typing, series of rapid events should be batched.

Debounced Storage Writer

// utils/debounce.js - Simple debounce implementation
function debounce(fn, delay) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// In options page - wait for user to stop typing before saving
const saveSettings = debounce((settings) => {
  chrome.storage.local.set({ settings });
}, 500);

// User typing in form inputs
document.querySelectorAll('input').forEach(input => {
  input.addEventListener('input', () => {
    saveSettings({ value: input.value });
  });
});

Search-as-You-Type in Popup

// popup/search.js
const search = debounce(async (query) => {
  const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
  renderResults(results);
}, 300);

document.getElementById('search').addEventListener('input', (e) => {
  search(e.target.value);
});

Throttle Patterns

Throttle limits execution frequency. Use when: need regular updates but not on every event.

Throttled DOM Observations

// content script - throttled DOM mutation observer
function throttle(fn, limit) {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

const observer = new MutationObserver(throttle((mutations) => {
  // Process batch of mutations
  handleMutations(mutations);
}, 100));

observer.observe(document.body, { childList: true, subtree: true });

Throttled Badge Updates

// background script - rate-limited badge updates
const updateBadge = throttle((count) => {
  chrome.action.setBadgeText({ text: count > 0 ? String(count) : '' });
}, 1000);

// Called frequently from content scripts
chrome.runtime.onMessage.addListener((msg) => {
  if (msg.type === 'UPDATE_COUNT') {
    updateBadge(msg.count);
  }
});

Throttled API Polling

// background script - limited API check frequency
const pollAPI = throttle(async () => {
  const data = await fetchLatestData();
  chrome.storage.local.set({ cachedData: data });
}, 60000); // Max once per minute

chrome.alarms.create('poll', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'poll') pollAPI();
});

Service Worker Timer Considerations

Service workers have unique constraints - setTimeout may not fire if the worker is suspended.

Use Chrome Alarms for > 30 Second Delays

// ❌ setTimeout may not fire when service worker is idle
setTimeout(doWork, 60000); // Unreliable

// ✅ Use chrome.alarms for reliable timing
chrome.alarms.create('work', { delayInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'work') doWork();
});

Debouncing chrome.storage.onChanged

// Batch rapid storage changes
const handleStorageChange = debounce((changes, area) => {
  // Process all changes at once
  Object.entries(changes).forEach(([key, { newValue }]) => {
    handleKeyChange(key, newValue);
  });
}, 100);

chrome.storage.onChanged.addListener((changes, area) => {
  handleStorageChange(changes, area);
});

Cross-Context Message Throttling

Message passing between extension contexts needs throttling to prevent congestion.

// Batched message sender
class BatchedMessenger {
  constructor(destination, batchSize = 10) {
    this.queue = [];
    this.destination = destination;
    this.batchSize = batchSize;
  }

  send(message) {
    this.queue.push(message);
    if (this.queue.length >= this.batchSize) {
      this.flush();
    } else if (!this.flushTimer) {
      this.flushTimer = setTimeout(() => this.flush(), 50);
    }
  }

  flush() {
    clearTimeout(this.flushTimer);
    this.flushTimer = null;
    if (this.queue.length > 0) {
      chrome.runtime.sendMessage(this.destination, this.queue);
      this.queue = [];
    }
  }
}

RequestAnimationFrame for Visual Updates

In content scripts, use requestAnimationFrame for smooth visual updates:

let pendingUpdate = null;

function scheduleUpdate(state) {
  if (!pendingUpdate) {
    pendingUpdate = requestAnimationFrame(() => {
      updateUI(state);
      pendingUpdate = null;
    });
  }
}

Quick Reference

Pattern Use Case Typical Delay
Debounce Storage writes, search 200-500ms
Throttle API polls, badge updates 1000ms+
RAF Visual updates Per frame
Alarms Long delays in SW > 30s

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