Реализация Background Service Worker в браузерном расширении
В Manifest V3 фоновый скрипт превратился в service worker. Это не просто переименование — изменилась модель жизненного цикла. Service worker может быть завершён браузером в любой момент, когда нет активных задач, и это ломает паттерны, которые работали в MV2 с persistent background page.
Регистрация в манифесте
{
"manifest_version": 3,
"background": {
"service_worker": "background/sw.js",
"type": "module"
}
}
type: "module" позволяет использовать ES-модули и import внутри service worker. Поддерживается в Chrome 93+, Firefox — с некоторыми ограничениями в MV2 (в MV3 для Firefox это тоже поддерживается с версии 101).
Жизненный цикл: что реально происходит
Браузер запускает service worker при:
- установке/обновлении расширения
- получении сообщения из content script или popup
- срабатывании alarm (chrome.alarms)
- наступлении сетевого события (если подписан)
После завершения всех обработчиков браузер может убить процесс через ~30 секунд неактивности. Следующее событие поднимет его снова — уже с чистым состоянием.
Это означает: никакого глобального состояния в памяти. Переменные не переживают перезапуск.
// ПЛОХО — состояние потеряется при перезапуске SW
let requestCount = 0;
chrome.runtime.onMessage.addListener(() => {
requestCount++; // после перезапуска SW снова 0
});
// ХОРОШО — сохраняем в chrome.storage
chrome.runtime.onMessage.addListener(async () => {
const { requestCount = 0 } = await chrome.storage.local.get('requestCount');
await chrome.storage.local.set({ requestCount: requestCount + 1 });
});
Обработка событий: синхронная регистрация обязательна
Обработчики событий должны быть зарегистрированы синхронно на верхнем уровне. Если вы регистрируете их внутри async-функции или после await — браузер может не «увидеть» их при запуске SW для обработки события:
// background/sw.js
// ПРАВИЛЬНО — синхронная регистрация на верхнем уровне
chrome.runtime.onInstalled.addListener(onInstalled);
chrome.runtime.onMessage.addListener(onMessage);
chrome.alarms.onAlarm.addListener(onAlarm);
chrome.tabs.onUpdated.addListener(onTabUpdated);
// Реализации могут быть async
async function onInstalled(details) {
if (details.reason === 'install') {
await chrome.storage.sync.set({ settings: defaultSettings });
}
if (details.reason === 'update') {
await migrateSettings(details.previousVersion);
}
}
function onMessage(message, sender, sendResponse) {
// Обязательно return true для async-ответов
handleMessage(message, sender).then(sendResponse);
return true;
}
Долгоживущие соединения через Port
Для задач, которые занимают больше нескольких секунд (стриминг, polling), используйте chrome.runtime.connect(). Активное соединение удерживает SW живым:
// background/sw.js
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'streaming-channel') return;
const controller = new AbortController();
port.onDisconnect.addListener(() => controller.abort());
streamData(port.sender.tab, controller.signal, (chunk) => {
port.postMessage({ type: 'CHUNK', data: chunk });
}).then(() => {
port.postMessage({ type: 'DONE' });
}).catch((err) => {
if (err.name !== 'AbortError') {
port.postMessage({ type: 'ERROR', message: err.message });
}
});
});
Работа с chrome.alarms для периодических задач
setTimeout и setInterval ненадёжны в service worker — они не переживают перезапуск. Для периодики:
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('sync-data', {
periodInMinutes: 15,
delayInMinutes: 1
});
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'sync-data') {
await syncWithServer();
}
});
async function syncWithServer() {
const { lastSync, authToken } = await chrome.storage.local.get(['lastSync', 'authToken']);
if (!authToken) return;
const response = await fetch('https://api.example.com/sync', {
method: 'POST',
headers: { Authorization: `Bearer ${authToken}` },
body: JSON.stringify({ since: lastSync })
});
if (response.ok) {
const data = await response.json();
await chrome.storage.local.set({
syncedData: data,
lastSync: Date.now()
});
// Уведомить content scripts на открытых вкладках
const tabs = await chrome.tabs.query({ url: 'https://*.example.com/*' });
tabs.forEach(tab => {
chrome.tabs.sendMessage(tab.id, { type: 'DATA_UPDATED', data });
});
}
}
Управление вкладками и навигация
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status !== 'complete') return;
if (!tab.url?.startsWith('https://target-site.com')) return;
// Inject script только когда страница полностью загружена
await chrome.scripting.executeScript({
target: { tabId },
func: initPageEnhancement,
args: [await getConfig()]
});
});
async function getConfig() {
const result = await chrome.storage.sync.get('config');
return result.config ?? {};
}
// Функция выполняется в контексте страницы (не SW)
function initPageEnhancement(config) {
window.__EXT_CONFIG__ = config;
document.dispatchEvent(new CustomEvent('ext:ready', { detail: config }));
}
Обработка ошибок и устойчивость
SW может быть убит в середине async-операции. Для критичных операций используйте транзакционный подход:
async function processQueue() {
const { queue = [] } = await chrome.storage.local.get('queue');
if (queue.length === 0) return;
const item = queue[0];
try {
await processItem(item);
// Успех — убираем из очереди
const { queue: current = [] } = await chrome.storage.local.get('queue');
await chrome.storage.local.set({ queue: current.slice(1) });
} catch (err) {
// Помечаем как неудачную попытку, но не убираем
const { queue: current = [] } = await chrome.storage.local.get('queue');
current[0] = { ...current[0], attempts: (current[0].attempts ?? 0) + 1, lastError: err.message };
await chrome.storage.local.set({ queue: current });
}
}
Взаимодействие с popup и options page
Popup и страница настроек существуют в отдельных документах, но могут общаться с SW через messaging. Для получения данных реального времени из SW в popup:
// popup/popup.js
const port = chrome.runtime.connect({ name: 'popup' });
port.onMessage.addListener((msg) => {
if (msg.type === 'STATE_UPDATE') updateUI(msg.state);
});
port.postMessage({ type: 'GET_STATE' });
// background/sw.js
const popupPorts = new Set();
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'popup') return;
popupPorts.add(port);
port.onDisconnect.addListener(() => popupPorts.delete(port));
port.onMessage.addListener(async (msg) => {
if (msg.type === 'GET_STATE') {
const state = await getAppState();
port.postMessage({ type: 'STATE_UPDATE', state });
}
});
});
// Рассылка обновлений всем открытым popup
function broadcastStateUpdate(state) {
for (const port of popupPorts) {
try { port.postMessage({ type: 'STATE_UPDATE', state }); }
catch {} // порт мог закрыться
}
}
Отладка service worker
В Chrome DevTools: расширение → «Service Worker» → «Inspect». SW имеет отдельный DevTools-контекст. При отладке следите за тем, что SW не убивается между точками останова — установите флаг «Update on reload» в панели Application.
В Firefox: about:debugging#/runtime/this-firefox → «Inspect» рядом с расширением.
Логи SW не видны в консоли страницы — только в консоли самого SW. Для передачи логов в popup используйте messaging или храните в chrome.storage.local.







