Broadcast Channel API Guide
Complete guide to communicating across browser tabs and windows with BroadcastChannel
The BroadcastChannel API is a simple but powerful way to send messages between browser tabs, windows, iframes, or workers — as long as they share the same origin. It's perfect for syncing state, sending alerts, or keeping things consistent across multiple views of your app.
How It Works
Here's a friendly breakdown:
1. Create a Channel
Give your channel a name:
const channel = new BroadcastChannel('my-dashboard');All tabs using 'my-dashboard' will be part of the same group.
2. Send a Message
Push messages out with postMessage():
channel.postMessage('User just logged in!');3. Listen for Messages
Other tabs can listen using the onmessage handler:
channel.onmessage = (event) => {
console.log('Received:', event.data);
};Real-World Examples
Multi-Tab Dashboard Sync
Let's say you're building a collaborative app or a multi-tab admin panel.
Tab A (Sender):
class DashboardSender {
constructor() {
this.channel = new BroadcastChannel('admin-sync');
}
sendNotification(message) {
this.channel.postMessage({
type: 'notification',
message,
timestamp: Date.now()
});
}
sendDataUpdate(data) {
this.channel.postMessage({
type: 'data-update',
data,
timestamp: Date.now()
});
}
sendUserAction(action) {
this.channel.postMessage({
type: 'user-action',
action,
userId: getCurrentUserId(),
timestamp: Date.now()
});
}
}
// Usage
const sender = new DashboardSender();
document.querySelector('#sendBtn').addEventListener('click', () => {
const msg = document.querySelector('#msgInput').value;
sender.sendNotification(msg);
});
// Send data updates
sender.sendDataUpdate({
users: updatedUsers,
stats: updatedStats
});Tab B (Receiver):
class DashboardReceiver {
constructor() {
this.channel = new BroadcastChannel('admin-sync');
this.setupListeners();
}
setupListeners() {
this.channel.onmessage = (event) => {
const { type, message, data, action, userId, timestamp } = event.data;
switch (type) {
case 'notification':
this.showNotification(message);
break;
case 'data-update':
this.updateDashboard(data);
break;
case 'user-action':
this.handleUserAction(action, userId);
break;
default:
console.log('Unknown message type:', type);
}
};
}
showNotification(message) {
// Create notification element
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
notification.remove();
}, 5000);
}
updateDashboard(data) {
// Update UI with new data
if (data.users) {
this.updateUserList(data.users);
}
if (data.stats) {
this.updateStats(data.stats);
}
}
handleUserAction(action, userId) {
console.log(`User ${userId} performed action: ${action}`);
// Handle specific user actions
}
updateUserList(users) {
// Update user list in UI
const userList = document.getElementById('user-list');
if (userList) {
userList.innerHTML = users.map(user =>
`<li>${user.name} - ${user.status}</li>`
).join('');
}
}
updateStats(stats) {
// Update statistics in UI
const statsElement = document.getElementById('stats');
if (statsElement) {
statsElement.textContent = `Active Users: ${stats.activeUsers}`;
}
}
}
// Initialize receiver
const receiver = new DashboardReceiver();Advanced Patterns
State Synchronization
class StateSync {
constructor(channelName = 'app-state') {
this.channel = new BroadcastChannel(channelName);
this.state = {};
this.listeners = new Set();
this.setupChannel();
}
setupChannel() {
this.channel.onmessage = (event) => {
const { type, payload, source } = event.data;
if (source === this.channel.name) return; // Avoid echo
switch (type) {
case 'state-update':
this.updateState(payload);
break;
case 'state-request':
this.sendState();
break;
case 'state-sync':
this.syncState(payload);
break;
}
};
}
updateState(newState) {
this.state = { ...this.state, ...newState };
this.notifyListeners();
}
setState(newState) {
this.state = { ...this.state, ...newState };
// Broadcast to other tabs
this.channel.postMessage({
type: 'state-update',
payload: newState,
source: this.channel.name
});
this.notifyListeners();
}
getState() {
return this.state;
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
notifyListeners() {
this.listeners.forEach(listener => listener(this.state));
}
sendState() {
this.channel.postMessage({
type: 'state-sync',
payload: this.state,
source: this.channel.name
});
}
syncState(newState) {
this.state = newState;
this.notifyListeners();
}
requestState() {
this.channel.postMessage({
type: 'state-request',
source: this.channel.name
});
}
}
// Usage
const stateSync = new StateSync('my-app');
// Subscribe to state changes
const unsubscribe = stateSync.subscribe((state) => {
console.log('State updated:', state);
updateUI(state);
});
// Update state (will broadcast to other tabs)
stateSync.setState({ user: { name: 'John', isOnline: true } });
// Request state from other tabs
stateSync.requestState();Authentication Sync
class AuthSync {
constructor() {
this.channel = new BroadcastChannel('auth-sync');
this.setupListeners();
}
setupListeners() {
this.channel.onmessage = (event) => {
const { type, payload } = event.data;
switch (type) {
case 'login':
this.handleLogin(payload);
break;
case 'logout':
this.handleLogout();
break;
case 'token-refresh':
this.handleTokenRefresh(payload);
break;
}
};
}
login(userData) {
// Store auth data locally
localStorage.setItem('auth', JSON.stringify(userData));
// Broadcast to other tabs
this.channel.postMessage({
type: 'login',
payload: userData
});
this.updateUI(userData);
}
logout() {
// Clear local auth data
localStorage.removeItem('auth');
// Broadcast to other tabs
this.channel.postMessage({
type: 'logout'
});
this.updateUI(null);
}
handleLogin(userData) {
// Update UI for login
this.updateUI(userData);
this.showNotification('Logged in from another tab');
}
handleLogout() {
// Update UI for logout
this.updateUI(null);
this.showNotification('Logged out from another tab');
}
handleTokenRefresh(newToken) {
// Update token
const auth = JSON.parse(localStorage.getItem('auth') || '{}');
auth.token = newToken;
localStorage.setItem('auth', JSON.stringify(auth));
this.showNotification('Token refreshed from another tab');
}
updateUI(userData) {
if (userData) {
document.body.classList.add('authenticated');
document.getElementById('user-name').textContent = userData.name;
} else {
document.body.classList.remove('authenticated');
document.getElementById('user-name').textContent = '';
}
}
showNotification(message) {
// Show notification to user
console.log(message);
}
}
// Usage
const authSync = new AuthSync();
// Login (will sync across tabs)
authSync.login({
name: 'John Doe',
email: 'john@example.com',
token: 'jwt-token-here'
});Form Data Sync
class FormSync {
constructor(formId, channelName = 'form-sync') {
this.form = document.getElementById(formId);
this.channel = new BroadcastChannel(channelName);
this.setupSync();
}
setupSync() {
// Listen for form changes
this.form.addEventListener('input', (e) => {
this.broadcastFormData();
});
// Listen for messages from other tabs
this.channel.onmessage = (event) => {
const { formData } = event.data;
this.updateForm(formData);
};
}
broadcastFormData() {
const formData = this.getFormData();
this.channel.postMessage({
type: 'form-update',
formData
});
}
getFormData() {
const formData = new FormData(this.form);
const data = {};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
return data;
}
updateForm(formData) {
// Update form fields without triggering events
Object.entries(formData).forEach(([key, value]) => {
const field = this.form.querySelector(`[name="${key}"]`);
if (field && field.value !== value) {
field.value = value;
field.classList.add('synced');
// Remove synced class after animation
setTimeout(() => {
field.classList.remove('synced');
}, 1000);
}
});
}
}
// Usage
const formSync = new FormSync('my-form');Important Considerations
Browser Support and Limitations
// Check for BroadcastChannel support
if (!window.BroadcastChannel) {
console.warn('BroadcastChannel not supported, falling back to localStorage');
// Implement fallback using localStorage + storage event
}
// Fallback implementation
class BroadcastChannelFallback {
constructor(channelName) {
this.channelName = channelName;
this.listeners = new Set();
this.setupStorageListener();
}
setupStorageListener() {
window.addEventListener('storage', (e) => {
if (e.key === this.channelName) {
const data = JSON.parse(e.newValue || '{}');
this.listeners.forEach(listener => listener(data));
}
});
}
postMessage(data) {
localStorage.setItem(this.channelName, JSON.stringify({
data,
timestamp: Date.now()
}));
}
addEventListener(type, listener) {
if (type === 'message') {
this.listeners.add(listener);
}
}
removeEventListener(type, listener) {
if (type === 'message') {
this.listeners.delete(listener);
}
}
close() {
this.listeners.clear();
}
}Message Size Limitations
// Check message size before sending
function checkMessageSize(message) {
const messageSize = new Blob([JSON.stringify(message)]).size;
const maxSize = 1024 * 1024; // 1MB limit
if (messageSize > maxSize) {
console.warn('Message too large, consider chunking or compression');
return false;
}
return true;
}
// Chunk large messages
function sendLargeMessage(channel, largeData) {
const chunks = chunkData(largeData, 100000); // 100KB chunks
chunks.forEach((chunk, index) => {
channel.postMessage({
type: 'chunked-data',
chunk,
index,
total: chunks.length,
id: Date.now()
});
});
}
function chunkData(data, chunkSize) {
const string = JSON.stringify(data);
const chunks = [];
for (let i = 0; i < string.length; i += chunkSize) {
chunks.push(string.slice(i, i + chunkSize));
}
return chunks;
}When to Use It
- Real-time status updates across tabs
- Logging a user out in all open tabs
- Syncing local storage changes
- Collaborative tools, dashboards, messaging UIs
- Form data synchronization across tabs
- Authentication state management
- App state synchronization
When Not to Use It
- Communicating across different domains → use
postMessagebetween windows or iframes instead - Long-term storage → use
localStorage,IndexedDB, or server APIs - Large data transfers → consider chunking or compression
- Frequent updates → consider throttling to avoid performance issues
Performance Best Practices
Message Throttling
class ThrottledBroadcastChannel {
constructor(channelName, throttleMs = 100) {
this.channel = new BroadcastChannel(channelName);
this.throttleMs = throttleMs;
this.pendingMessages = new Map();
}
postMessage(data) {
const key = JSON.stringify(data);
if (this.pendingMessages.has(key)) {
// Message already pending, don't send again
return;
}
this.pendingMessages.set(key, setTimeout(() => {
this.channel.postMessage(data);
this.pendingMessages.delete(key);
}, this.throttleMs));
}
}Memory Management
// Clean up channels when not needed
function cleanupChannel(channel) {
if (channel) {
channel.close();
}
}
// Clean up on page unload
window.addEventListener('beforeunload', () => {
cleanupChannel(myChannel);
});Final Thoughts
The BroadcastChannel API is a lightweight, easy-to-use tool when you want quick communication across tabs. It's great for real-time coordination in modern web apps without the overhead of server-side messaging or complex setups.
Key benefits:
- Simple API - easy to implement and understand
- Real-time communication - instant message delivery
- No server required - works entirely in the browser
- Cross-tab synchronization - perfect for multi-tab applications
Remember to handle browser support gracefully and consider message size limitations for optimal performance! 🌐💬
Next: ARIA Roles and Attributes - Learn how to implement proper ARIA for custom UI components.