Learning Guides
Menu

Browser APIs

17 min readFrontend Patterns & Concepts

Browser APIs

Modern browsers provide powerful APIs that enable rich, app-like experiences. Understanding these APIs helps you build performant, capable web applications.

Web Storage

Web Storage provides a way to store key-value pairs in the browser. Unlike cookies, data stored here isn't sent with every HTTP request, making it more efficient for client-side data persistence.

localStorage and sessionStorage

localStorage persists data indefinitely until explicitly cleared—even after closing the browser. It's ideal for:

  • User preferences (theme, language)
  • Authentication tokens (though HttpOnly cookies are safer)
  • Cached data that should survive page refreshes

sessionStorage works identically but clears when the browser tab closes. Use it for:

  • Temporary form data
  • Page-specific state that shouldn't persist
  • Multi-step wizard progress

Both have a ~5MB storage limit per origin and are synchronous (can block the main thread with large data).

JAVASCRIPT
// localStorage - persists across sessions
localStorage.setItem("user", JSON.stringify({ id: 1, name: "John" }));
const user = JSON.parse(localStorage.getItem("user"));
localStorage.removeItem("user");
localStorage.clear();
 
// sessionStorage - cleared when tab closes
sessionStorage.setItem("tempData", "value");
 
// Storage wrapper with expiry
class StorageWithExpiry {
  constructor(storage = localStorage) {
    this.storage = storage;
  }
 
  set(key, value, ttlMs) {
    const item = {
      value,
      expiry: ttlMs ? Date.now() + ttlMs : null,
    };
    this.storage.setItem(key, JSON.stringify(item));
  }
 
  get(key) {
    const itemStr = this.storage.getItem(key);
    if (!itemStr) return null;
 
    const item = JSON.parse(itemStr);
 
    if (item.expiry && Date.now() > item.expiry) {
      this.storage.removeItem(key);
      return null;
    }
 
    return item.value;
  }
 
  remove(key) {
    this.storage.removeItem(key);
  }
}
 
const cache = new StorageWithExpiry();
cache.set("apiData", data, 60 * 60 * 1000); // 1 hour expiry

Storage Events

When a user has multiple tabs open, you often need to keep them in sync. If a user logs out in one tab, all other tabs should reflect that immediately. The storage event fires when localStorage changes in another tab (not the current one), enabling cross-tab communication.

Common use cases:

  • Logout synchronization across tabs
  • Theme/preference updates
  • Real-time data sync without WebSockets
  • Shopping cart updates
JAVASCRIPT
// Listen for changes from other tabs
window.addEventListener("storage", (e) => {
  console.log({
    key: e.key,
    oldValue: e.oldValue,
    newValue: e.newValue,
    url: e.url, // Which tab made the change
  });
 
  if (e.key === "user") {
    // Sync user state across tabs
    updateUserState(JSON.parse(e.newValue));
  }
});
 
// Cross-tab communication
function broadcastLogout() {
  localStorage.setItem("logout", Date.now().toString());
  localStorage.removeItem("logout");
}

IndexedDB

While localStorage is simple, it has significant limitations:

  • Only stores strings (must JSON.stringify/parse objects)
  • ~5MB limit
  • Synchronous API blocks the main thread
  • No indexing or querying capabilities

IndexedDB solves these problems by providing:

  • Storage of any JavaScript value (including Blobs, Files)
  • Much larger storage limits (often 50%+ of disk space)
  • Asynchronous API that doesn't block UI
  • Indexes for fast lookups on any property
  • Transactions for data integrity

When to use IndexedDB:

  • Offline-first applications (PWAs)
  • Large datasets (thousands of records)
  • Complex data relationships
  • Storing files and binary data
  • When you need to query data by multiple fields

The API is callback-based and complex, so here's a Promise-based wrapper that makes it easier to use:

JAVASCRIPT
class Database {
  constructor(name, version = 1) {
    this.name = name;
    this.version = version;
    this.db = null;
  }
 
  async open(upgradeCallback) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.name, this.version);
 
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this);
      };
 
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        upgradeCallback(db, event.oldVersion, event.newVersion);
      };
    });
  }
 
  async get(storeName, key) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeName, "readonly");
      const store = transaction.objectStore(storeName);
      const request = store.get(key);
 
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
 
  async put(storeName, value) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeName, "readwrite");
      const store = transaction.objectStore(storeName);
      const request = store.put(value);
 
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
 
  async getAll(storeName, query, count) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeName, "readonly");
      const store = transaction.objectStore(storeName);
      const request = store.getAll(query, count);
 
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
 
  async delete(storeName, key) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeName, "readwrite");
      const store = transaction.objectStore(storeName);
      const request = store.delete(key);
 
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}
 
// Usage
const db = new Database("MyApp", 1);
 
await db.open((database, oldVersion) => {
  if (oldVersion < 1) {
    const store = database.createObjectStore("users", { keyPath: "id" });
    store.createIndex("email", "email", { unique: true });
  }
});
 
await db.put("users", { id: 1, name: "John", email: "john@example.com" });
const user = await db.get("users", 1);

Web Workers

JavaScript is single-threaded—long-running computations block the UI, causing janky scrolling, unresponsive buttons, and frustrated users. If a function takes 500ms to execute, the browser literally freezes for that duration.

Web Workers solve this by running JavaScript in a background thread. The worker runs in parallel with the main thread, communicating via message passing. This keeps the UI responsive while heavy work happens in the background.

Ideal use cases for Web Workers:

  • Image/video processing
  • Large data transformations (sorting, filtering millions of rows)
  • Complex calculations (cryptography, physics simulations)
  • Parsing large JSON or CSV files
  • Syntax highlighting for code editors

Limitations to be aware of:

  • No DOM access (workers can't manipulate the page directly)
  • No access to window, document, or parent scope variables
  • Communication happens via structured cloning (copying data, not sharing)
  • Each worker is a separate file (though inline workers work around this)

Basic Worker

Here's the fundamental pattern: the main thread creates a worker, sends data to it, and receives results back via messages.

JAVASCRIPT
// worker.js
self.onmessage = function (e) {
  const { type, data } = e.data;
 
  switch (type) {
    case "PROCESS":
      const result = heavyComputation(data);
      self.postMessage({ type: "RESULT", result });
      break;
  }
};
 
function heavyComputation(data) {
  // CPU-intensive work here
  return data.map((item) => complexTransform(item));
}
 
// main.js
const worker = new Worker("worker.js");
 
worker.onmessage = (e) => {
  const { type, result } = e.data;
  if (type === "RESULT") {
    displayResults(result);
  }
};
 
worker.onerror = (error) => {
  console.error("Worker error:", error);
};
 
// Send work to worker
worker.postMessage({ type: "PROCESS", data: largeDataset });
 
// Clean up
worker.terminate();

Inline Worker (No Separate File)

Creating a separate file for each worker can be cumbersome, especially for simple tasks. You can create workers dynamically using Blob URLs. This pattern embeds the worker code directly in your main file, making it easier to bundle and deploy.

JAVASCRIPT
function createWorker(fn) {
  const blob = new Blob(
    [`self.onmessage = function(e) { (${fn.toString()})(e.data); }`],
    { type: "application/javascript" },
  );
 
  return new Worker(URL.createObjectURL(blob));
}
 
const worker = createWorker((data) => {
  const result = data.numbers.reduce((a, b) => a + b, 0);
  self.postMessage(result);
});
 
worker.onmessage = (e) => console.log("Sum:", e.data);
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });

Worker Pool

Creating a new worker for each task is expensive—workers take time to initialize and consume memory. A Worker Pool maintains a fixed number of reusable workers, distributing tasks among them and queuing excess work.

Why use a pool?

  • Avoids worker creation overhead for repeated tasks
  • Limits memory usage by capping concurrent workers
  • Automatically distributes work evenly
  • Provides a clean Promise-based API

The pool size typically matches navigator.hardwareConcurrency (number of CPU cores) for optimal parallelism.

JAVASCRIPT
class WorkerPool {
  constructor(workerScript, size = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.queue = [];
    this.activeJobs = new Map();
 
    for (let i = 0; i < size; i++) {
      const worker = new Worker(workerScript);
      worker.onmessage = (e) => this.handleMessage(worker, e);
      worker.onerror = (e) => this.handleError(worker, e);
      this.workers.push({ worker, busy: false });
    }
  }
 
  run(data) {
    return new Promise((resolve, reject) => {
      const job = { data, resolve, reject };
 
      const available = this.workers.find((w) => !w.busy);
      if (available) {
        this.dispatch(available, job);
      } else {
        this.queue.push(job);
      }
    });
  }
 
  dispatch(workerInfo, job) {
    workerInfo.busy = true;
    this.activeJobs.set(workerInfo.worker, job);
    workerInfo.worker.postMessage(job.data);
  }
 
  handleMessage(worker, e) {
    const job = this.activeJobs.get(worker);
    if (job) {
      job.resolve(e.data);
      this.activeJobs.delete(worker);
    }
 
    const workerInfo = this.workers.find((w) => w.worker === worker);
 
    if (this.queue.length > 0) {
      this.dispatch(workerInfo, this.queue.shift());
    } else {
      workerInfo.busy = false;
    }
  }
 
  handleError(worker, error) {
    const job = this.activeJobs.get(worker);
    if (job) {
      job.reject(error);
      this.activeJobs.delete(worker);
    }
  }
 
  terminate() {
    this.workers.forEach((w) => w.worker.terminate());
  }
}
 
// Usage
const pool = new WorkerPool("processor.js", 4);
 
const results = await Promise.all([
  pool.run({ task: "process", data: chunk1 }),
  pool.run({ task: "process", data: chunk2 }),
  pool.run({ task: "process", data: chunk3 }),
  pool.run({ task: "process", data: chunk4 }),
]);

Service Workers

Service Workers are a special type of Web Worker that acts as a proxy between your app and the network. They intercept every network request, allowing you to:

  • Cache responses for offline access
  • Serve cached content when the network is slow or unavailable
  • Update content in the background without user interaction
  • Push notifications even when the app isn't open

Key characteristics:

  • Lives beyond page lifetime (persists even after tab closes)
  • Only works on HTTPS (or localhost for development)
  • Has its own lifecycle (install → activate → fetch)
  • Cannot access DOM (uses postMessage to communicate)

The Service Worker lifecycle:

  1. Register: Browser downloads and parses the SW file
  2. Install: SW caches essential assets
  3. Activate: SW takes control (old SWs are removed)
  4. Fetch: SW intercepts network requests
JAVASCRIPT
// Registering a service worker
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("/sw.js")
    .then((registration) => {
      console.log("SW registered:", registration.scope);
    })
    .catch((error) => {
      console.error("SW registration failed:", error);
    });
}
 
// sw.js
const CACHE_NAME = "app-v1";
const ASSETS = ["/", "/index.html", "/styles.css", "/app.js", "/offline.html"];
 
// Install - cache assets
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then((cache) => cache.addAll(ASSETS))
      .then(() => self.skipWaiting()),
  );
});
 
// Activate - clean old caches
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => key !== CACHE_NAME)
            .map((key) => caches.delete(key)),
        ),
      )
      .then(() => self.clients.claim()),
  );
});
 
// Fetch - serve from cache or network
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      if (cached) {
        return cached;
      }
 
      return fetch(event.request)
        .then((response) => {
          // Cache successful responses
          if (response.ok) {
            const clone = response.clone();
            caches
              .open(CACHE_NAME)
              .then((cache) => cache.put(event.request, clone));
          }
          return response;
        })
        .catch(() => {
          // Offline fallback
          if (event.request.mode === "navigate") {
            return caches.match("/offline.html");
          }
        });
    }),
  );
});

Broadcast Channel

While the storage event only fires for localStorage changes, Broadcast Channel provides a dedicated API for cross-tab/window communication. It's simpler and more explicit than the localStorage hack.

Why use Broadcast Channel instead of storage events?

  • Works with any data type (not just strings)
  • Fires in real-time without storage side effects
  • Cleaner API with explicit channel names
  • Also works between iframes and workers

Common use cases:

  • Synchronized logout across tabs
  • Real-time preference updates
  • Collaborative features (multiple tabs editing same document)
  • Sharing state without server round-trips
JAVASCRIPT
// Create channel
const channel = new BroadcastChannel("app-messages");
 
// Send message
channel.postMessage({
  type: "USER_LOGGED_OUT",
  timestamp: Date.now(),
});
 
// Receive messages
channel.onmessage = (event) => {
  const { type, ...data } = event.data;
 
  switch (type) {
    case "USER_LOGGED_OUT":
      clearLocalSession();
      redirectToLogin();
      break;
    case "THEME_CHANGED":
      updateTheme(data.theme);
      break;
  }
};
 
// Close when done
channel.close();

Clipboard API

The Clipboard API provides secure, asynchronous access to read and write clipboard content. It replaced the synchronous document.execCommand('copy') which had security concerns and inconsistent behavior.

Why the modern Clipboard API?

  • Asynchronous (returns Promises)
  • Requires user permission for reading
  • Works in secure contexts (HTTPS) only
  • Can handle rich content (images, HTML)
  • Better security model

Permission notes:

  • Writing (copy) usually works without explicit permission if triggered by user action
  • Reading (paste) always requires permission
  • Browsers show permission prompts automatically
JAVASCRIPT
// Write to clipboard
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    showNotification("Copied!");
  } catch (err) {
    // Fallback for older browsers
    const textarea = document.createElement("textarea");
    textarea.value = text;
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand("copy");
    document.body.removeChild(textarea);
  }
}
 
// Read from clipboard
async function pasteFromClipboard() {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error("Failed to read clipboard:", err);
  }
}
 
// Copy rich content
async function copyRichContent() {
  const blob = new Blob(["<b>Rich</b> content"], { type: "text/html" });
  const item = new ClipboardItem({ "text/html": blob });
  await navigator.clipboard.write([item]);
}

Geolocation

The Geolocation API lets you access the user's geographical location (with their permission). It uses various sources—GPS, Wi-Fi positioning, IP geolocation—to determine position.

Common use cases:

  • Store locators ("Find stores near me")
  • Delivery apps (tracking, estimated arrival)
  • Weather apps (local forecasts)
  • Maps and navigation
  • Location-based content personalization

Privacy considerations:

  • Always requires explicit user permission
  • Only works in secure contexts (HTTPS)
  • Users can deny or revoke permission anytime
  • Consider providing manual location entry as fallback

Accuracy options:

  • enableHighAccuracy: true uses GPS (slower, more battery, more precise)
  • enableHighAccuracy: false uses network-based location (faster, less precise)
JAVASCRIPT
function getCurrentPosition(options = {}) {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error("Geolocation not supported"));
      return;
    }
 
    navigator.geolocation.getCurrentPosition(
      (position) => {
        resolve({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy,
          timestamp: position.timestamp,
        });
      },
      (error) => {
        reject(error);
      },
      {
        enableHighAccuracy: options.highAccuracy || false,
        timeout: options.timeout || 5000,
        maximumAge: options.maxAge || 0,
      },
    );
  });
}
 
// Watch position
function watchPosition(callback, errorCallback) {
  const id = navigator.geolocation.watchPosition(callback, errorCallback, {
    enableHighAccuracy: true,
  });
 
  // Return cleanup function
  return () => navigator.geolocation.clearWatch(id);
}
 
// Usage
const stopWatching = watchPosition(
  (position) => updateMap(position.coords),
  (error) => console.error(error),
);
 
// Later: stopWatching();

Notifications API

The Notifications API displays system-level notifications outside the browser window. Unlike in-app toasts, these appear even when the user is in another application.

When to use notifications:

  • New messages in chat applications
  • Background task completion
  • Important alerts (price drops, reminders)
  • Real-time updates (breaking news)

Best practices:

  • Ask for permission at the right moment (not immediately on page load)
  • Provide value—don't spam users with trivial notifications
  • Use tag to replace similar notifications (avoid notification pile-up)
  • Include actions users can take directly from the notification

Permission states:

  • default: User hasn't decided yet
  • granted: User allowed notifications
  • denied: User blocked notifications (can't ask again)
JAVASCRIPT
async function requestNotificationPermission() {
  if (!("Notification" in window)) {
    console.log("Notifications not supported");
    return false;
  }
 
  if (Notification.permission === "granted") {
    return true;
  }
 
  if (Notification.permission !== "denied") {
    const permission = await Notification.requestPermission();
    return permission === "granted";
  }
 
  return false;
}
 
async function showNotification(title, options = {}) {
  const permitted = await requestNotificationPermission();
  if (!permitted) return;
 
  const notification = new Notification(title, {
    body: options.body,
    icon: options.icon || "/icon.png",
    badge: options.badge,
    tag: options.tag, // Replace existing with same tag
    requireInteraction: options.requireInteraction || false,
    data: options.data,
  });
 
  notification.onclick = (event) => {
    event.preventDefault();
    window.focus();
    options.onClick?.(event);
    notification.close();
  };
 
  if (options.autoClose) {
    setTimeout(() => notification.close(), options.autoClose);
  }
 
  return notification;
}

History API

The History API enables client-side navigation without full page reloads—the foundation of single-page applications (SPAs). It lets you change the URL, maintain browser history (back/forward buttons work), and store state for each history entry.

Why this matters:

  • Better UX: No page reload flicker, preserved scroll position
  • Shareable URLs: Deep linking to specific app states
  • Browser navigation: Back/forward buttons work correctly
  • SEO friendly: When combined with SSR, crawlers see proper URLs

Key methods:

  • pushState(): Add new entry to history stack (navigating forward)
  • replaceState(): Modify current entry (fixing typos in URL, filters)
  • popstate event: Fires when user clicks back/forward

Important: pushState changes the URL but doesn't trigger any navigation—your code must handle rendering the new state.

JAVASCRIPT
// Push new state
history.pushState(
  { page: "products", id: 123 }, // State object
  "", // Title (ignored by most browsers)
  "/products/123", // URL
);
 
// Replace current state
history.replaceState(
  { page: "products", filter: "active" },
  "",
  "/products?filter=active",
);
 
// Listen for back/forward
window.addEventListener("popstate", (event) => {
  if (event.state) {
    // Navigate based on state
    navigateTo(event.state.page, event.state);
  }
});
 
// Simple router
class Router {
  constructor() {
    this.routes = new Map();
 
    window.addEventListener("popstate", () => {
      this.handleRoute(location.pathname);
    });
  }
 
  register(path, handler) {
    this.routes.set(path, handler);
  }
 
  navigate(path, state = {}) {
    history.pushState(state, "", path);
    this.handleRoute(path);
  }
 
  handleRoute(path) {
    // Check for exact match
    if (this.routes.has(path)) {
      this.routes.get(path)();
      return;
    }
 
    // Check for pattern match
    for (const [pattern, handler] of this.routes) {
      const match = this.matchPattern(pattern, path);
      if (match) {
        handler(match.params);
        return;
      }
    }
 
    // 404
    this.routes.get("*")?.();
  }
 
  matchPattern(pattern, path) {
    const patternParts = pattern.split("/");
    const pathParts = path.split("/");
 
    if (patternParts.length !== pathParts.length) return null;
 
    const params = {};
 
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i].startsWith(":")) {
        params[patternParts[i].slice(1)] = pathParts[i];
      } else if (patternParts[i] !== pathParts[i]) {
        return null;
      }
    }
 
    return { params };
  }
}

Page Visibility API

The Page Visibility API tells you when a page is visible (user is looking at it) or hidden (tab is in background, browser is minimized). This is crucial for optimizing performance and user experience.

Why track visibility?

  • Save resources: Pause animations, videos, and polling when tab is hidden
  • Accurate analytics: Know actual time spent viewing content
  • Better UX: Resume smoothly when user returns
  • Battery life: Stop CPU-intensive work when not needed

Visibility states:

  • visible: Page is in the foreground tab of a non-minimized window
  • hidden: Page is not visible (background tab, minimized window, lock screen)

Common applications:

  • Pause video/audio playback
  • Stop real-time data polling
  • Pause game loops
  • Defer non-critical updates
  • Track engagement metrics accurately
JAVASCRIPT
// Detect when page is visible/hidden
document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    // Page is hidden - pause expensive operations
    pauseVideo();
    pauseAnimations();
    disconnectWebSocket();
  } else {
    // Page is visible again
    resumeVideo();
    resumeAnimations();
    reconnectWebSocket();
  }
});
 
// Check current state
if (document.visibilityState === "visible") {
  startPolling();
}
 
// Use for analytics
let startTime;
 
document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    const duration = Date.now() - startTime;
    analytics.track("page_visible_duration", { duration });
  } else {
    startTime = Date.now();
  }
});

Network Information API

JAVASCRIPT
if ("connection" in navigator) {
  const connection = navigator.connection;
 
  console.log({
    effectiveType: connection.effectiveType, // '4g', '3g', '2g', 'slow-2g'
    downlink: connection.downlink, // Mbps
    rtt: connection.rtt, // Round-trip time in ms
    saveData: connection.saveData, // Data saver enabled
  });
 
  // Adapt to network conditions
  if (connection.saveData || connection.effectiveType === "2g") {
    loadLowQualityImages();
    disableAutoplay();
  }
 
  // Listen for changes
  connection.addEventListener("change", () => {
    console.log("Connection changed:", connection.effectiveType);
    adaptToNetwork(connection);
  });
}

Battery Status API

JAVASCRIPT
if ("getBattery" in navigator) {
  navigator.getBattery().then((battery) => {
    console.log({
      level: battery.level, // 0 to 1
      charging: battery.charging,
      chargingTime: battery.chargingTime,
      dischargingTime: battery.dischargingTime,
    });
 
    // Adapt to battery level
    if (battery.level < 0.2 && !battery.charging) {
      enableLowPowerMode();
    }
 
    battery.addEventListener("levelchange", () => {
      updateBatteryUI(battery.level);
    });
 
    battery.addEventListener("chargingchange", () => {
      if (battery.charging) {
        disableLowPowerMode();
      }
    });
  });
}

Summary

APIPurposeCommon Use Cases
localStoragePersistent key-valueUser preferences, tokens
sessionStorageSession key-valueForm drafts, temp data
IndexedDBClient-side databaseOffline data, caching
Web WorkersBackground threadsHeavy computation
Service WorkersRequest proxyOffline support, caching
Broadcast ChannelCross-tab messagingSession sync
ClipboardCopy/pasteShare, export
GeolocationUser locationMaps, local content
NotificationsSystem notificationsAlerts, updates
HistoryURL managementSPA routing
VisibilityTab focus stateResource management

Key Principles:

  1. Check for API support before using
  2. Handle permissions gracefully
  3. Provide fallbacks for unsupported browsers
  4. Clean up resources when done
  5. Consider privacy implications