Back to list
dev_to 2026年3月14日

Hacky クロスタブコードを書くのが嫌だったから、Omnitab を作りました。

I Got Tired of Writing Hacky Cross-Tab Code. So I Built Omnitab.

Translated: 2026/3/14 12:01:53
omnitabcross-tabweb-developmentbrowser-compatibilityjavascript-libraries

Japanese Translation

複数のタブ間で同期させたいウェブアプリを作ったことがあれば、その苦痛を分かります。 カートが一つのタブで更新されても、別のタブはそのことに気づかないままです。ユーザがテーマを変更しても、それが他のタブに反映されません。セッションが期限切れになったのに、開かれている別の three つのタブは元気がよく「すべて正常です」とpretends しています。 毎回この問題に直面すると、同じ脆弱なコードのバリエーションを作成してしまうのです。localStorage イベントリスナーはダクトで接合され、ブラウザ間で一貫性のない挙動を示し、量産环境でも破綻しないことにゼロの自信しかありません。既存ライブラリは、私の必要以上に過剰機能を持っているか、静かに Safari で壊れているのです。 そのため、ワザワザ迂回路に手がを伸ばすのをやめ、Omnitab という軽量で依存なしのクロスタブメッセンジャーライブラリを作りました。これにはすぐには機能します。 ブラウザタブは設計上で隔離されています。どこでも機能するネイティブな「他のすべてのタブにメッセージを送る」API はありません。代わりに、三つの異なるメカニズムがあり、それぞれ独自のブラウザサポートギャップを持っています。 Transport 仕組み 制限 SharedWorker タブをまたぐ共有 JS コンテキスト 利用範囲が限られている BroadcastChannel ネイティブパブリッシュ/サブスク IE11 にはなし StorageEvent localStorage 変化に便乗する ユニバーサルだが、間接的 多くのソリューションは一つを選んでそのままにします。つまり、Safari ユーザーを落とすか、知らぬ間に信頼性の低いものにもうが落ち込むのです。 Omnitab は起動時にブラウザ環境を評価し、自動的に利用可能な最適なトランスポートを選択します: SharedWorker → BroadcastChannel → StorageEvent (最も速い) (ネイティブ) (ユニバーサル) この設定は行いません。考えもしません。それが正しいものを選びます。 import { createBus } from 'omnitab'; const bus = createBus('my-app'); bus.subscribe('cart:update', (cart) => { renderCart(cart); }); bus.publish('cart:update', { items: [...], total: 59.99 }); 基本的な使用の API はこれだけです。バスを作成する関数一つ、publish する関数一つ、subscribe する関数一つ。 ユーザはあなたの店舗を二つのタブで開きます。一つのタブでアイテムを追加すると、他のタブはその変更を即座に反映し、ページのリフレッシュなしに。 const bus = createBus('shop'); bus.subscribe('cart:update', (cart) => { renderCart(cart); }); function updateCart(newCart) { saveCart(newCart); bus.publish('cart:update', newCart); } ユーザは一つのタブで「ログアウト」をクリックします。他のすべての開いているタブ应立即に反応するはずです - 古いセッションなし、セキュリティのギャップなし。 const bus = createBus('auth'); bus.subscribe('auth:logout', () => { clearLocalSession(); window.location.href = '/login'; }); function logout() { clearLocalSession(); bus.publish('auth:logout'); window.location.href = '/login'; } ユーザはダークモードを切り替えます。すべてのタブ应立即に切り替わり、クリックしたタブだけでないはずです。 const bus = createBus('ui'); bus.subscribe('theme:change', (theme) => { document.documentElement.dataset.theme = theme; }); function setTheme(theme) { document.documentElement.dataset.theme = theme; bus.publish('theme:change', theme); } Omnitab は違いを処理するので、あなたがそれを心配する必要はありません。 Browser SharedWorker BroadcastChannel StorageEvent Chrome (デスクトップ) ✅ ✅ ✅ Firefox (デスクトップ) ✅ ✅ ✅ Safari (デスクトップ) ✅ 16+ ✅ ✅ Safari on iOS ✅ 16+ ✅ ✅ Edge ✅ ✅ ✅ Chrome for Android ❌ ✅ ✅ IE11 ❌ ❌ ✅ Samsung Internet ❌ ✅ ✅ 初期化時、Omnitab はトランスポートの選択を示すコンソールレポートを記録します。そのため、常に裏側で何が起こっているかが分かっています。 基本的な機能を超えて、Omnitab は信頼性を必要とするアプリのための機能を含んでいます。 健康チェック:定期的によりトランスポートが活性かを検証し、それが静かになった場合、再接続します。 メッセージキューとリトライ:送信が失敗した場合、そのメッセージをキューに入れ、指数関数的バックオフで再試行します。 ストレージの安全さ:localStorage フェールバックを使用する場合、Omnitab はTTL、サイズ制限、調整可能なエビクションポリシーを適用して、 runaway storage growth を防止します。 const bus = createBus('my-app', { enableHealthChecks: true, enableMessageQueue: true, maxRetries: 5, retryDelay: 500, retryBackoff: 2,

Original Content

If you've ever built a web app where multiple tabs need to stay in sync you know the pain. The cart updates in one tab, but the other tab has no idea. The user switches themes, but the change doesn't reflect elsewhere. A session expires, but three other open tabs are happily pretending everything is fine. Every time I hit this problem, I'd write some variation of the same brittle code. localStorage event listeners duct-taped together, inconsistent behavior across browsers, and zero confidence it'd hold up in production. Existing libraries were either overkill for what I needed or quietly broke in Safari. So I stopped reaching for workarounds and built Omnitab a lightweight, zero-dependency cross-tab messaging library that just works. Browser tabs are isolated by design. There's no native "send a message to all my other tabs" API that works everywhere. Instead, there are three different mechanisms - each with their own browser support gaps: Transport How it works Limitation SharedWorker A shared JS context across tabs Limited availability BroadcastChannel Native pub/sub between tabs Not in IE11 StorageEvent Piggybacks on localStorage changes Universal, but indirect Most solutions pick one and call it a day. That means you're either dropping Safari users or falling back to something unreliable without knowing it. Omnitab evaluates the browser environment on startup and automatically selects the best available transport: SharedWorker → BroadcastChannel → StorageEvent (fastest) (native) (universal) You don't configure this. You don't think about it. It just picks the right one. import { createBus } from 'omnitab'; const bus = createBus('my-app'); bus.subscribe('cart:update', (cart) => { renderCart(cart); }); bus.publish('cart:update', { items: [...], total: 59.99 }); That's the entire API for basic usage. One function to create a bus, one to publish, one to subscribe. A user opens your store in two tabs. They add an item in one tab and the other tab should reflect that immediately without a page refresh. const bus = createBus('shop'); bus.subscribe('cart:update', (cart) => { renderCart(cart); }); function updateCart(newCart) { saveCart(newCart); bus.publish('cart:update', newCart); } User clicks "Log out" in one tab. Every other open tab should respond immediately - no stale sessions, no security gaps. const bus = createBus('auth'); bus.subscribe('auth:logout', () => { clearLocalSession(); window.location.href = '/login'; }); function logout() { clearLocalSession(); bus.publish('auth:logout'); window.location.href = '/login'; } User toggles dark mode. All tabs should switch instantly and not just the one they clicked in. const bus = createBus('ui'); bus.subscribe('theme:change', (theme) => { document.documentElement.dataset.theme = theme; }); function setTheme(theme) { document.documentElement.dataset.theme = theme; bus.publish('theme:change', theme); } Omnitab handles the differences so you don't have to. Browser SharedWorker BroadcastChannel StorageEvent Chrome (desktop) ✅ ✅ ✅ Firefox (desktop) ✅ ✅ ✅ Safari (desktop) ✅ 16+ ✅ ✅ Safari on iOS ✅ 16+ ✅ ✅ Edge ✅ ✅ ✅ Chrome for Android ❌ ✅ ✅ IE11 ❌ ❌ ✅ Samsung Internet ❌ ✅ ✅ On initialization, Omnitab logs a console report showing which transport was selected. So you always know what's running under the hood. Beyond the basics, Omnitab includes features for apps that need reliability: Health checks : periodically verifies the transport is alive, and reconnects if it goes silent. Message queue with retry : if a send fails, it queues the message and retries with exponential backoff. Storage safety : when using the localStorage fallback, Omnitab enforces TTLs, size limits, and configurable eviction policies to prevent runaway storage growth. const bus = createBus('my-app', { enableHealthChecks: true, enableMessageQueue: true, maxRetries: 5, retryDelay: 500, retryBackoff: 2, worker: { connectTimeout: 3000, heartbeatInterval: 5000, }, storage: { ttl: 8000, evictionPolicy: 'oldest', onStorageFull: (err) => console.warn('Storage full:', err), }, }); npm install omnitab Full documentation, API reference, and configuration options are on npm. If you've ever written a hacky localStorage event listener just to sync two tabs then I hope this saves you from writing it again. Feedback, issues, and contributions are welcome. If you find it useful, a GitHub star goes a long way. 🙏