Синхронизация вкладок браузера с помощью BroadcastChannel
BroadcastChannel синхронизирует вкладки в реальном времени через structured clone, с очисткой, fallback и шаблонами для auth, корзины и темы.
BroadcastChannel API — это встроенная шина сообщений браузера, позволяющая вкладкам, окнам, iframe и воркерам одного источника (origin) обмениваться данными в режиме реального времени. Это целевое решение проблемы рассинхронизации состояния между вкладками: когда выход из системы в одной вкладке оставляет остальные по-прежнему отображающими интерфейс авторизованного пользователя. API состоит из четырёх вызовов и не требует сервера, процедуры установления соединения (handshake) или сторонних зависимостей. В статье рассматриваются минимальный набор API, паттерн сообщений с типами действий, который хорошо масштабируется, интеграция с фреймворками с корректной очисткой ресурсов, неочевидные подводные камни, а также текущая поддержка браузерами с работающим запасным вариантом (fallback).
Ключевые выводы
- Весь API состоит из четырёх вызовов —
new BroadcastChannel(name),postMessage(data),onmessageиclose()— и не требует сервера, handshake или какой-либо конфигурации. - Поскольку
postMessageиспользует алгоритм структурированного клонирования (structured clone algorithm), объекты, Map, Set и Blob передаются напрямую — безJSON.stringifyи ручного разбора на принимающей стороне. - Вкладка никогда не получает собственные широковещательные сообщения: они доставляются во все слушающие каналы кроме объекта-отправителя, что исключает циклы обратной связи, от которых иначе пришлось бы защищаться вручную.
- BroadcastChannel — это канал передачи, а не хранилище: он транспортирует сообщения, но ничего не сохраняет. Используйте его совместно с localStorage или IndexedDB для обеспечения надёжности хранения и инициализации вкладок, открытых после наступления события.
- BroadcastChannel входит в Baseline и широко доступен в Chrome, Firefox, Edge, Safari, Opera и Samsung Internet начиная с марта 2022 года; поддержка Safari появилась в версии 15.4. Единственный браузер, в котором он отсутствует, — Internet Explorer.
Что такое рассинхронизация состояния между вкладками?
Рассинхронизация состояния между вкладками (multi-tab state drift) — это класс ошибок, при которых две открытые вкладки одного приложения содержат расходящееся состояние: пользователь выходит из системы в одной вкладке, а другая по-прежнему отображает панель управления; или очищает корзину в одной вкладке, а в другой товары остаются. Записи сессий (session replays) таких сценариев регулярно фиксируют видимый симптом — пользователь оформляет заказ из корзины, уже очищенной в другой вкладке, или продолжает работу во вкладке, из которой должен был выйти. Именно эту рассинхронизацию устраняет BroadcastChannel.
Разработчики решали эту проблему тремя менее эффективными способами. Событие storage объекта localStorage срабатывает между вкладками, но передаёт только строки, что вынуждает сериализовать и разбирать каждое сообщение и неудобно управлять ключами. Периодический опрос сервера или localStorage расточителен и создаёт задержки — большинство проверок ничего не обнаруживают. SharedWorker и WebSocket — полноценные инструменты, однако передавать данные через сервер по сети для синхронизации двух вкладок на одной машине — избыточно для сугубо локальной задачи.
| Механизм | Тип данных | Сохраняет данные? | Нужна сеть? | Лучше всего подходит для |
|---|---|---|---|---|
| BroadcastChannel | Structured clone (объекты, Map, Blob) | Нет | Нет | Локальная синхронизация вкладок/воркеров одного источника |
Событие storage | Только строки | Да (localStorage) | Нет | Простая синхронизация со встроенной надёжностью хранения |
| SharedWorker | Structured clone | Нет | Нет | Общие вычисления/соединения между вкладками |
| WebSocket | Строки/бинарные данные | На стороне сервера | Да | Серверные push-данные в реальном времени для нескольких клиентов |
BroadcastChannel API в четырёх вызовах
Discover how at OpenReplay.com.
Весь API состоит из четырёх вызовов, и любой контекст, создающий канал с одним и тем же именем, подключается к одной шине. Вы подключаетесь, слушаете, отправляете и закрываете:
const channel = new BroadcastChannel("app-sync");
channel.onmessage = (event) => {
console.log("Received:", event.data);
};
channel.postMessage({ hello: "world" });
channel.close(); // при размонтировании / завершении работы
Главное преимущество — тип передаваемых данных. Данные, отправленные через postMessage, сериализуются с помощью алгоритма структурированного клонирования, поэтому объекты, массивы, Map, Set и Blob передаются напрямую без строковой сериализации — получатель читает event.data как готовый объект. Исключения, которые не поддерживают клонирование, — Symbol и SharedArrayBuffer. Каналы работают только в пределах одного источника (same-origin), и важно переиспользовать один экземпляр канала на контекст: создание нового BroadcastChannel при каждой отправке приводит к утечке слушателей.
Как структурировать сообщения BroadcastChannel?
Масштабируемый паттерн — дискриминированное сообщение вида { type, payload } — плюс один слушатель, который переключается по type и применяет каждое действие к локальному состоянию. Это позволяет всем контекстам работать по единому протоколу; сам API не придаёт сообщениям никакого смысла — его определяете вы.
const channel = new BroadcastChannel("cart-sync");
let cart = [];
channel.onmessage = ({ data }) => {
if (data.type === "ADD_ITEM") cart.push(data.payload);
else if (data.type === "CLEAR") cart = [];
render();
};
function addItem(item) {
cart.push(item); // немедленно обновляем состояние в текущей вкладке
render();
channel.postMessage({ type: "ADD_ITEM", payload: item });
}
Обратите внимание: отправитель обновляет собственное состояние напрямую перед широковещательной рассылкой — поскольку вкладка не получает собственные сообщения, локальное изменение применяется inline, а широковещательная рассылка распространяется на все остальные вкладки.
Синхронизация авторизации, корзины и настроек между вкладками
Наиболее ценные сценарии использования — синхронизация сессии (широковещательный выход из системы очищает все вкладки), синхронизация корзины, настроек и темы оформления, а также автозаполнение форм в нескольких вкладках. В React создавайте канал внутри useEffect и возвращайте функцию очистки, вызывающую close(); без этого каждое повторное монтирование будет оставлять активный слушатель.
import { useEffect } from "react";
function useAuthSync(onLogout) {
useEffect(() => {
const channel = new BroadcastChannel("auth");
channel.onmessage = ({ data }) => {
if (data.type === "LOGOUT") onLogout();
};
return () => channel.close();
}, [onLogout]);
}
// при выходе из системы: очищаем локальную сессию, затем
// new BroadcastChannel("auth").postMessage({ type: "LOGOUT" })
В Svelte 5 (runes) оберните канал в store. Важный нюанс: postMessage требует обычный клонируемый объект, а не реактивный прокси, поэтому перед отправкой передайте значение через $state.snapshot — прокси не пройдёт этап структурированного клонирования.
// theme.svelte.js — Svelte 5 (runes)
export class ThemeStore {
value = $state("light");
#channel = new BroadcastChannel("theme");
constructor() {
this.#channel.onmessage = ({ data }) => { this.value = data; };
}
set(next) {
this.value = next;
this.#channel.postMessage($state.snapshot(next));
}
}
Подводные камни, о которых большинство руководств умалчивает
Четыре особенности поведения часто приводят к ошибкам в реализации, и именно о них большинство материалов предпочитает не говорить:
- Вкладка не получает собственные широковещательные сообщения. Сообщения доставляются во все слушающие каналы, кроме объекта-отправителя — это поведение определено спецификацией, и оно полезно: оно предотвращает бесконечные циклы обратной связи, от которых иначе пришлось бы защищаться проверкой идентификатора отправителя. Применяйте изменение состояния отправителя inline.
- «Один источник» означает «один раздел хранилища». Хранилище разделяется по сайту верхнего уровня (top-level site), поэтому cross-site iframe не разделяет канал со страницей верхнего уровня даже при одинаковом источнике, а каналы никогда не объединяют разные поддомены.
- Это канал передачи, а не хранилище. BroadcastChannel транспортирует сообщения, но ничего не сохраняет. Используйте его совместно с localStorage или IndexedDB — как для надёжности хранения, так и для инициализации вкладок, открытых после наступления события: вкладка, не получившая широковещательное сообщение, при монтировании читает сохранённое состояние.
- Всегда вызывайте
close()при размонтировании. Закрытие отключает объект и освобождает его для сборки мусора; канал, созданный в компоненте и никогда не закрытый, оставляет слушателя при каждом повторном монтировании.
Поддержка браузерами и fallback с определением возможностей
BroadcastChannel входит в Baseline и широко доступен начиная с марта 2022 года; актуальный Safari полностью его поддерживает — утверждение об отсутствии поддержки в WebKit устарело и относится к версиям до Safari 15.4. Согласно caniuse и блогу Chrome for Developers, минимальные версии: Chrome 54+, Edge 79+, Firefox 38+, Safari 15.4+ (macOS и iOS), Opera 41+, Samsung Internet 7.2+; только Internet Explorer никогда не получил поддержку.
Для браузеров до 2022 года определяйте наличие возможности через 'BroadcastChannel' in window и используйте shim на основе события storage, предоставляющий тот же интерфейс:
function createChannel(name) {
if ("BroadcastChannel" in window) return new BroadcastChannel(name);
// Запасной вариант: событие "storage" объекта localStorage (только строки)
return {
postMessage: (data) =>
localStorage.setItem(name, JSON.stringify({ data, t: Date.now() })),
set onmessage(fn) {
window.addEventListener("storage", (e) => {
if (e.key === name && e.newValue) fn({ data: JSON.parse(e.newValue).data });
});
},
close() {},
};
}
В первую очередь используйте нативный API, а shim рассматривайте как страховку. Во всех актуальных браузерах BroadcastChannel уже доступен — выберите одно имя канала, стандартизируйте формат { type, payload }, сохраняйте то, что должно пережить полное закрытие, и проблема устаревших данных во вкладках вашего приложения исчезнет.
Часто задаваемые вопросы
Работает ли BroadcastChannel между разными поддоменами?
Нет. BroadcastChannel ограничен одним разделом хранилища, а не просто одним источником, и никогда не объединяет разные поддомены. Страница на app.example.com и страница на account.example.com не могут использовать общий канал. Даже в пределах одного источника cross-site iframe не разделяет канал со страницей верхнего уровня, поскольку хранилище разделяется по сайту верхнего уровня. Для синхронизации между поддоменами используйте сервер или WebSocket.
В чём разница между BroadcastChannel и событием storage для синхронизации между вкладками?
BroadcastChannel передаёт данные через structured clone — объекты, Map, Set и Blob напрямую, — но ничего не сохраняет. Событие storage объекта localStorage передаёт только строки, что вынуждает сериализовать и разбирать каждое сообщение, однако в качестве побочного эффекта записывает долговременное состояние. Используйте BroadcastChannel для богатой, событийно-ориентированной синхронизации и сочетайте его с localStorage или IndexedDB, когда требуется также обеспечить персистентность или инициализировать вкладки, открытые после наступления события.
Получает ли вкладка-отправитель собственное сообщение BroadcastChannel?
Нет. Согласно спецификации, сообщение доставляется во все объекты BroadcastChannel, слушающие канал, кроме объекта-отправителя. Вкладка никогда не получает собственные широковещательные сообщения, что предотвращает бесконечные циклы обратной связи, от которых иначе пришлось бы защищаться проверкой идентификатора отправителя. Именно поэтому изменение состояния отправителя следует применять inline перед вызовом postMessage, а широковещательная рассылка распространяется на все остальные контексты.
Что происходит с сообщениями BroadcastChannel, если все вкладки закрыты?
Они теряются. BroadcastChannel — это канал передачи, а не хранилище: он транспортирует сообщения, но ничего не сохраняет, поэтому любое состояние, переданное широковещательно в момент, когда другие вкладки не слушали, будет утеряно после закрытия всех экземпляров. Вкладка, открытая после наступления события, никогда не получит исходное сообщение. Чтобы избежать этого, сохраняйте состояние в localStorage или IndexedDB и инициализируйте поздно открываемые вкладки из этого хранилища при монтировании, не полагаясь исключительно на канал.