MV2 vs MV3: Event Page vs Service Worker
Manifest V2 — Event Page (Firefox)
Manifest V2 — Event Page (Firefox)
In Manifest V2 the background context is a persistent event page loaded from a list of scripts. The
backgroundIncludeBrowserExt array in gulpfile.js (plus background.js itself) defines which scripts are concatenated. The page persists for the lifetime of the browser session and has full access to DOM APIs.Manifest V3 — Service Worker (Chrome / Edge)
Manifest V3 — Service Worker (Chrome / Edge)
In Manifest V3 the background becomes a service worker, which Chrome can terminate at any time to save memory. The entry point is The
src/browserExt/background-worker.js:/*BACKGROUND SCRIPTS*/ placeholder is filled by the build pipeline with the same backgroundIncludeBrowserExt script list. keep-mv3-alive.js and background.js are appended after the shared scripts.Service workers do not have a
window object and cannot use browser APIs that require a live DOM (e.g. DOMParser). Those operations are delegated to an offscreen document instead — see the Offscreen Document section below.What the Background Process Does
Translator Cache (Zotero.Translators)
Zotero.Translators (src/common/translators.js) loads translator metadata from extension preferences on startup and maintains four in-memory caches keyed by type (import, export, web, search):
keepTranslatorsUpdated() runs a recursive timer loop that calls updateFromRemote() every 24 hours (ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL). Metadata is first requested from the Zotero desktop client; if unavailable, it falls back to the zotero.org translator repository.
URL-Match Translator Detection
When a new page URL is reported (Zotero.Translators.getWebTranslatorsForLocation(url, rootURL)), the background iterates over the web cache and tests each translator’s target regexp against the URL. Only translators that match are returned to the injected script for the heavier detectWeb() phase.
Extension Toolbar Icon & Badge
Zotero.Connector_Browser._updateExtensionUI(tab) in src/browserExt/background.js calls browser.action.setIcon() and browser.action.setTitle() to reflect the current tab state:
Connector Preferences (Zotero.Prefs)
Zotero.Prefs wraps browser.storage to provide synchronous get/set and asynchronous getAsync access to extension preferences. Preferences include translatorMetadata, firstUse, automaticSnapshots, and per-translator code caches.
Routing Translated Items
When a save is triggered,Zotero.Connector.callMethod(endpoint, data) (src/common/connector.js) POSTs to http://127.0.0.1:23119/connector/<endpoint>. If the request fails with HTTP status 0 (connection refused), isOnline is set to false and Zotero.Connector.onStateChange(false) fires, which disables Zotero.ContentTypeHandler and causes subsequent saves to fall back to the zotero.org API.
Background Script Bundle
The full background bundle (fromgulpfile.js) is:
Offscreen Document (MV3 Only)
Chrome MV3 service workers lack a DOM, so any code that needsDOMParser (used by translators) must run in an offscreen document — a hidden, tab-less HTML page that Chrome allows a service worker to create.
Zotero.OffscreenManager (src/browserExt/background/offscreenManager.js) owns the lifecycle:
offscreen/offscreen.html + offscreen/offscreen.js) hosts the full translation sandbox. When a translation is requested, the background’s OffscreenManager.sendMessage(message, payload, tab, frameId) forwards the work to the offscreen page via MessagingGeneric, receives the result, and relays it back to the inject script.
The
offscreen permission must be declared in manifest-v3.json to use browser.offscreen.createDocument(). The offscreen document is subject to Chrome’s idle-cleanup policy and may be destroyed; offscreenManager handles re-creation transparently.Keeping the MV3 Service Worker Alive
Chrome terminates idle service workers after roughly 30 seconds.src/browserExt/keep-mv3-alive.js runs a polling loop to prevent this during active operations:
Zotero.Connector_Browser.shouldKeepServiceWorkerAlive() returns truthy whenever long-running tasks are active (e.g. a Google Docs integration HTTP round-trip). The internal counter _keepServiceWorkerAlive is mutated by a reference-counted setter:
Message Listener Registration
Zotero.Messaging.init() in src/common/messaging.js registers the central browser.runtime.onMessage listener that handles all inject-to-background RPC calls:
receiveMessage() splits the message name on ".", resolves Zotero[NAMESPACE][METHOD], applies any configured background.postReceive / background.preSend hooks from MESSAGES, and returns the result to the inject side.
Background Initialization Sequence
WhenZotero.initGlobal() is called at the bottom of background.js:
1
Core namespaces initialized
Zotero.Prefs, Zotero.HTTP, Zotero.Connector, and other shared modules are set up.2
Messaging initialized
Zotero.Messaging.init() registers the browser.runtime.onMessage listener.3
Translators initialized
Zotero.Translators.init() loads cached metadata from preferences or fetches from the remote repo.4
Offscreen document created (MV3/Chromium only)
Zotero.Connector_Browser.init() calls Zotero.OffscreenManager.init(), which creates the offscreen document and waits for it to signal readiness.5
Tab info persistence (MV3)
_tabInfo is hydrated from Zotero.Utilities.Connector.createMV3PersistentObject('tabInfo', …) so per-tab translator state survives service-worker restarts.6
Web navigation listeners attached
browser.webNavigation.onCommitted, onDOMContentLoaded, and onHistoryStateUpdated are registered to track page navigations and trigger translator detection.