How to Create Offscreen Documents in Chrome Extensions: A Complete Guide

Published: (December 15, 2025 at 08:40 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Introduction

If you’re building Chrome extensions with Manifest V3, you’ve likely encountered the challenge of service workers terminating frequently. Offscreen documents provide a hidden HTML environment that can maintain persistent state and perform continuous operations without being visible to the user.

What Is an Offscreen Document?

An offscreen document is a hidden HTML page that runs in the background of your extension. Unlike service workers, which are short‑lived to save resources, offscreen documents stay alive, allowing you to:

  • Keep persistent data structures (e.g., Map, Set)
  • Perform heavy computations on large datasets
  • Process audio/video streams that require continuous operation

Think of it as an invisible webpage that your extension controls, offering a stable environment for long‑running tasks.

Ideal Use Cases

  • Persistent Data Processing – Maintain in‑memory data that would be lost when a service worker stops.
  • Heavy Computations – Run CPU‑intensive algorithms without interruption.
  • Audio/Video Processing – Handle media that needs ongoing access to memory.

Important Limitations

  • Cannot access the host website’s DOM (no buttons, content, etc.).
  • Available only in Manifest V3 extensions for Chrome (not supported in Firefox).
  • Runs in an isolated context separate from content scripts.
  • Can only use the chrome.runtime API for messaging; other APIs such as storage or tabs are unavailable.

Manifest Declaration

Add the offscreen permission to your manifest.json:

{
  "manifest_version": 3,
  "name": "Your Extension Name",
  "version": "1.0.0",
  "description": "A short description of your extension.",
  "permissions": ["offscreen"],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  }
}

Offscreen HTML File (offscreen.html)

Create this file in the extension root. It never appears to the user but provides the execution environment.

<!DOCTYPE html>
<html>
  <head>
    <title>Offscreen Document</title>
  </head>
  <body></body>
</html>

Offscreen Script (offscreen.js)

Handle persistent operations and listen for messages from the background script.

// Perform heavy processing or maintain persistent state
console.log('Offscreen document is live');

// Listen for messages from the background or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'Blah_Blah') {
    // Perform your processing here
    // ...

    // Example response
    sendResponse({ result: 'processed' });
  }
  // Return true to keep the message channel open for async response
  return true;
});

Note: Offscreen console logs appear in the same inspector as the service worker. Open chrome://extensions/, enable Developer mode, and click the “service worker” link to view them.

Creating the Offscreen Document (background.js)

let creatingOffscreen = null;

async function ensureOffscreen() {
  const offscreenUrl = chrome.runtime.getURL('offscreen.html');

  // Check if an offscreen document already exists
  if ('getContexts' in chrome.runtime) {
    const contexts = await chrome.runtime.getContexts({
      contextTypes: ['OFFSCREEN_DOCUMENT'],
      documentUrls: [offscreenUrl],
    });

    if (contexts.length > 0) {
      console.log('Offscreen document already exists');
      return;
    }
  } else {
    // Fallback for older Chrome versions
    const clients = await self.clients.matchAll();
    if (clients.some(client => client.url.includes(chrome.runtime.id))) {
      return;
    }
  }

  // Create the offscreen document if it doesn't exist
  if (!creatingOffscreen) {
    creatingOffscreen = chrome.offscreen.createDocument({
      url: 'offscreen.html',
      reasons: ['WORKERS'],
      justification: 'Maintain persistent data structures for extension functionality',
    });

    await creatingOffscreen;
    creatingOffscreen = null;
    console.log('Offscreen document created successfully');
  } else {
    // Wait for an ongoing creation to finish
    await creatingOffscreen;
  }
}

// Ensure offscreen document on startup
chrome.runtime.onStartup.addListener(() => {
  ensureOffscreen();
  console.log('Extension started, offscreen document ensured');
});

// Ensure offscreen document on installation
chrome.runtime.onInstalled.addListener(() => {
  ensureOffscreen();
  console.log('Extension installed, offscreen document created');
});

// Handle explicit requests to ensure the offscreen document exists
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg && msg.type === 'ensure_offscreen') {
    ensureOffscreen()
      .then(() => sendResponse({ ok: true }))
      .catch(err => sendResponse({ ok: false, error: String(err) }));
    return true; // Keep channel open for async response
  }
});

How ensureOffscreen Works

  1. Check existence – Uses chrome.runtime.getContexts() (preferred) or self.clients.matchAll() (fallback).
  2. Prevent duplicate creation – A shared creatingOffscreen promise avoids race conditions.
  3. Create if needed – Calls chrome.offscreen.createDocument() with the required url, reasons, and justification.

Offscreen Creation Parameters

  • url – Path to your offscreen HTML file.
  • reasons – One or more valid reasons from the Chrome API (e.g., WORKERS, AUDIO_PLAYBACK, DOM_PARSER, etc.).
  • justification – Clear explanation for why the document is needed; this is reviewed during extension submission.

Closing Offscreen Documents

When the document is no longer required, you can close it to free resources:

async function closeOffscreen() {
  try {
    await chrome.offscreen.closeDocument();
    console.log('Offscreen document closed');
  } catch (error) {
    console.error('Error closing offscreen document:', error);
  }
}

Messaging Between Background and Offscreen

Use the Chrome runtime messaging API to communicate.

From background to offscreen:

chrome.runtime.sendMessage(
  { type: 'Blah_Blah' },
  response => {
    console.log('Response from offscreen:', response);
  }
);

Browser Support & Characteristics

  • Manifest version – Offscreen documents are only available in Manifest V3 (Chrome and other Chromium‑based browsers). Not supported in Firefox.
  • DOM access – Cannot interact directly with the host page’s DOM; all data must be transferred via messaging.
  • Persistence – State is retained between operations, unlike service workers.
  • Visibility – Completely hidden from users; no UI elements are displayed.

Additional Resources

  • Official Chrome documentation on offscreen documents.
  • Feel free to explore the extFast boilerplate for a ready‑made extension setup that includes authentication, subscription handling, and more.
Back to Blog

Related posts

Read more »