JOURNAL

スマホのブラウザ通知が「届いたり届かなかったり」する理由を調べてみた

「タイマーが終了した瞬間に、スマホの画面にポンと通知を出したい」

タイマーやアラーム系のツールを作っていると、ごく自然にこの要望が出てきます。そして意気揚々とNotifications APIを実装し、意気揚々と実機テストをした瞬間……「あれ? 通知、届くときと届かないときがあるな……?」なんで?となります。

赤猫座の道具(SPLIT TIMERなど)を開発していたとき、まさにこの「Web通知の信頼性問題」に直面し、夜な夜なスマホを睨みつけながら丸2日溶かしました。今回は、Webブラウザの通知がスマホで届かない理由と、最終的にたどり着いた現実的な解決策を詳しくまとめます。

1. モバイル通知を阻む「3大トラップ」

① iOS Safariの「ホーム画面追加」という高すぎる壁

長年待ち望まれ、iOS 16.4からようやくiPhoneでもWeb Push通知が使えるようになりました。しかし、ここには開発者を絶望させる大きな前提条件があります。

それは、「Safariのブラウザタブで開いているだけでは通知許可が取れず、ユーザーがわざわざ『ホーム画面に追加(PWA化)』した状態で起動しないと動かない」という仕様です。

Webツールを「開いてすぐ手軽に使いたい」と思っているライトユーザーに対して、「まず共有ボタンを押して、下にスクロールしてホーム画面に追加し、そこから開き直してください」と要求するのは、あまりにもハードルが高すぎます。

② Notifications APIの「tag重複」で通知が握りつぶされる怪現象

AndroidやPCでテストしていた際、「1回目のタイマーは鳴るのに、2回目や短時間のタイマーがなぜか無言でスルーされる」という謎のバグに半日悩まされました。

犯人は、通知オプションに指定していた tag 属性でした。

// 危険な例:同じtagを使い回すと古い通知に上書き・抑制される
new Notification('1分経過しました!', {
  body: '次のラップに進みます。',
  tag: 'split-timer-alert' // ← 同じtagだとOSに「重複」とみなされて消える!
});

// 安全な例:毎回ユニークなIDを付与するか、tagを省略する
new Notification('1分経過しました!', {
  body: '次のラップに進みます。',
  tag: 'split-timer-' + Date.now()
});

OSやブラウザは、画面が通知で埋め尽くされないように「同じtagの通知が来たら前の通知を上書き(または静かに破棄)」します。これを知らずに固定文字列を渡していたため、短時間の連続タイマーが次々と握りつぶされていたのでした。

③ 一度「拒否」されたら二度と出せないワンチャン勝負

Web通知の許可ダイアログは、ユーザーが一度「ブロック(許可しない)」を押してしまうと、JavaScript側からは二度とダイアログを再表示できません。ユーザー自身にブラウザの深い設定画面を開いてもらい、手動で権限をリセットしてもらわない限り永遠に沈黙します。

いきなり Notification.requestPermission() を呼ぶと高確率で拒否されるため、事前に「通知を許可するとタイマー終了時にお知らせできます」という丁寧な自前モーダル(ソフトプロンプト)を挟むなどの工夫が必須になります。

2. 最終結論:通知を当てにせず「音(Web Audio API)」をメインに据える

コード側でできる限りの対策を施しても、最終的にはOSの省電力機能やスリープ状態、ユーザーの権限設定という「Webの外側の事情」に左右されます。時間を計るツールにおいて、「鳴るかもしれないし鳴らないかもしれない」というギャンブル設計は致命的です。

そこで赤猫座のSPLIT TIMERでは、以下のように割り切った設計にシフトしました。

  • 画面通知(Notification):許可されたらラッキー程度の「サブのおまけ」に格下げ。
  • 音・ビープ音(Web Audio API):通知許可がなくても確実に鳴らせる「主役の通知手段」に昇格。
// Web Audio APIなら事前の権限許可なしでクリアな警告音を鳴らせる
function playBeep(frequency = 880, duration = 0.2) {
  const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();

  osc.type = 'sine';
  osc.frequency.value = frequency;
  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.start();
  gain.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + duration);
  osc.stop(audioCtx.currentTime + duration);
}

Web Audio APIによる電子音(ビープ音)であれば、重い音声ファイルをロードする必要もなく、通知権限の許可をユーザーに乞う必要もありません(※最初の画面タップ時にAudioContextを一度アンロックしておく作法だけ必要です)。

通知手段到達の確実性ユーザーの手間・心理的負担総評・おすすめ度
Web Push通知(iOS)△(PWA追加時のみ)✕(ホーム画面追加が必須で超面倒)△ Webアプリ化前提ならあり
Notifications API○(Android/PCなら良好)△(拒否されたら即終了の罠)○ 補助的な通知として併用
Web Audio API(ビープ音)◎(タブが生きていれば100%)◎(権限許可不要で手軽)★ タイマー系ツールの本命
画面フラッシュ・アニメーション◎(見ている間は100%)◎(完全自動で邪魔にならない)★ 視覚補助として必須

完璧を目指すより「確実に伝わる手段」を選ぶ

最新のWeb APIを全部盛りにして「どんな環境でも完璧にPush通知を飛ばすぞ!」と意気込むのは技術者として楽しいものですが、OSのセキュリティや省電力の壁を前にすると、時にシンプルな「音」や「画面の点滅」の方が圧倒的に頼りになります。

「道具として本当にユーザーを裏切らない手段はどれか?」を突き詰めた結果としての音メイン設計。丸2日悩んで出したこの結論は、今でも間違っていなかったなと思っています。