2023年後半頃から、ブラウザの「戻る」ボタンを押すと、訪問したおぼえのないページが表示されることが増えた。そういうページは大抵、記事風の広告やサイト内の記事へのリンクが大量に並ぶという構成になっている。
この手法はブラウザバック広告とかブラウザバックレコメンド (あるいはレコメンデーション) とか呼ばれており、国内外の複数のWeb広告会社がこれを提供しているようだ。
たとえば、こちらはGMOアドマーケティングの “TAXEL” が提供しているブラウザバックレコメンド。
サイトから離れてしまうユーザーに対し、広告やレコメンド記事を表示させることで、収益化や内部回遊に繋げることを目的としているフォーマットになります。
……というのがセールスポイントらしいのだが、サイトから離れる人は、サイトから離れたいのである。ブラウザの「戻る」ボタンは「戻る」ためにある。それを押しても戻れない。ブラウザのUIが想定外の動きをする。困ったことである。
国内だと、他にもGENIEE SSPがこの種のブラウザバックレコメンドソリューションを売っているらしい。
ジーニーが「GENIEE SSP」内に「ブラウザバックレコメンド」を搭載 離脱を防ぎ、回遊を促す:MarkeZine(マーケジン)
じっさい一番よく見かけるのがOutbrain社のブラウザバック広告。この機能を直接紹介している公式ページがなかなか見つからないので紹介しづらいのだが、ブラウザバック広告の右上にこんなロゴが表示される。
こんなことがどうして (技術的に) 可能かというと、ブラウザのHistory APIを使い、ブラウザの履歴に勝手にアイテムをねじ込むことで実現されている。どのように動くのか、ブラウザ側では何か対策がされているのか、少し調べてみた。
試してみる
例を見てみよう。Impressの運営するサイトの多くでは、Outbrain社によるブラウザバックレコメンドが採用されている。
(追記 : こちらは冒頭で紹介したGMOアドマーケティングのサービスとは別物。あちらは、ブラウザバック広告がメディア向けにどんな風に売られているかを紹介するために取り上げた。)
広告画面の右上に「Recommended by Outbrain」と表示される。以下のページなどで現物を見ることができる (今回例として挙げさせてもらったけれど、記事の内容自体は面白い。筆者の方ごめんなさい)。
フィルムカメラの使い方 いろいろと考えて操作する楽しさ – Impress Watch
ブラウザで新規ウィンドウで開き、履歴のまっさらな状態からスタートすると挙動がわかりやすい。Google Chromeだと発動しない場合がある。ブラウザによってちょっと挙動が違う。
- Firefox : 「戻る」ボタンを押すと「こちらもおすすめ」という画面 (ブラウザバック広告) が表示される。新規タブで開いた場合は、さいしょ無効化されていた「戻る」ボタンが数秒後にピコンと活性化する瞬間が見られるよ!
- Safari & Edge
- 開いてすぐ「戻る」ボタンを押すと、ブラウザバック広告は表示されない。ページを離脱して実際にさっき開いていたページに戻る、もしくは「戻る」ボタンが無効化されている。
- ページ内のどこか適当な場所をクリックまたはタップしてから「戻る」ボタンを押すと、「こちらもおすすめ」という画面が表示される。
- 新規タブで開いてこれを試すと、ページのどこかをクリックした時点で「戻る」ボタンが活性する瞬間を観察できるよ!
- この挙動は、後述のように、履歴の取り扱いに制限があるため。
- Chrome : 発動する条件がいまいちわからない。同じ操作をしても出たり出なかったりして、再現させるのが難しい。後述のように、Chromeは「History APIの濫用を防ぐための何らかの施策」をしているらしい。
ブラウザのデバッグツールを使ってwindow.history.length
とwindow.history.state
の現在の値を監視していると、履歴の操作が行われる際のヘンな動作が観察できる。
- Firefox : Web Developer Tools > Debugger > Watch Expressions
- Chrome: Developer Tools > Sources > Watch
- Safari : Show JavaScript Console > Sources > Scope Chainタブ > Watch Expressions
を使うと、window.history
の様子をチェックできる。ブラウザバック広告の出ない一般サイトと挙動を比べてみると、window.history.length
の増え方がひとつ多い。
Hisoty APIと、その存在意義
検証を進める前に、History APIについて簡単な説明をする。
History APIというのは、ブラウザの「履歴」を取り扱うためのAPIである。
たとえば、history.pushState
関数を呼び出せば、JavaScriptを使って履歴を登録することができる。悪用も可能なのでいろいろ制約はあるが、正しく使えれば便利な機能だ。
最も活躍するのは、ページの遷移を挟まずに画面を動的に書き換えていくSPA (single page application) だろう。正しく使われれば、「ひとつ前」に戻ろうとして戻るボタンを押したときに、ユーザの意図どおり「さっき表示していた内容」に戻ることができるようになる。これは、うっかりアプリ自体から離脱しまうよりもずっとユーザの意図に沿った挙動といえるだろう。
実装を覗いてみる
さきほど例として挙げたImpressのサイトから呼ばれているのはOutbrain社の広告スクリプトであった。その中からhistory.pushState
を呼び出している箇所を調べてみた。
難読化されているが、整形し、ChatGPTに解説をお願いした。
uh = function(a) {
const b = () => {
// 履歴の長さが1かどうかを確認し、結果をa.kvに設定
a.kv = window.history.length === 1;
// 現在の履歴の状態を取得
const currentState = window.history.state;
// AndroidデバイスでFacebookのIn-App Browserを使用しているかをチェック
if (OBR.i.gb.search(/android/gim) >= 0 &&
(OBR.i.gb.search(/fb_iab/gim) >= 0 || OBR.i.gb.search(/fbav/gim) >= 0)) {
// 条件に一致する場合はpushStateを使用
window.history.pushState({ obem: 1 }, "");
} else {
// それ以外の場合はreplaceStateを使用
window.history.replaceState({ obem: 1 }, "");
}
// a.options.cMが真の場合、現在のURLで新しい履歴エントリを追加
if (a.options.cM) {
window.history.pushState(null, null, window.location.href);
}
// 最初に保存した履歴の状態を再度pushStateで追加
window.history.pushState(currentState, "");
};
// Safariであること、特定の条件が真である場合にイベントリスナーを設定
if (!th(a) && "safari" === OBR.i.$b && OBR.i.Py && OBR.i.Py >= 16 && a.options.wK) {
let eventTriggered = false;
window.addEventListener("touchend", () => {
if (!eventTriggered) {
eventTriggered = true;
b();
}
}, { once: true });
window.addEventListener("mousedown", () => {
if (!eventTriggered) {
eventTriggered = true;
b();
}
}, { once: true })
} else {
// それ以外の場合は直接関数bを実行
b();
}
return a;
};
SPAの実装をしたことのあるWebフロントエンドプログラマならすぐにピンとくるコードだと思う。
history.pushState
を呼ぶ際、第一引数に履歴のステートを保持するためのオブジェクトを渡すことができるのだが、ここに { obem: 1 }
を渡し、その後に再び「実際に今見ているページとステート」をプッシュしている。こうすると、「さっきまで『現在と同一のURLで、 { obem: 1 }
というステートを持った状態』を閲覧していた」という履歴を作り出すことができるのである。
この履歴が追加された状態でブラウザの戻るボタンを押すと、戻る先は実際にさっきまで見ていたページではなく「現在のURLで、 { obem: 1 }
というステートを持った状態」になる。その際、URLは変更されないのでアンロード & リロードは起こらず、popstate
イベントが発生。このpopstate
イベントのハンドラ内でwindow.history.state
を読み取って、さっきセットされていた { obem: 1 }
という値の存在を確認し、ページ全体にかぶせるようなレイヤーとして広告が特盛になってるページを表示するという仕組みだ。
window.history.length
とwindow.history.state
を観察してみると動きがわかりやすい。
ブラウザ仕様との攻防
Safariおよび一定の条件を満たしたユーザに対して特別な処理をしているところに注目してほしい。Safariの場合、いきなりhistoryの操作を行う (上記のコードでは const b
として定義されている関数) のではなく、画面のタップやクリックをトリガーとして履歴の操作を行う実装になっている。何故か。
if (!th(a) && "safari" === OBR.i.$b && OBR.i.Py && OBR.i.Py >= 16 && a.options.wK) {
let eventTriggered = false;
window.addEventListener("touchend", () => {
if (!eventTriggered) {
eventTriggered = true;
b(); // この "b" という関数の内部でhistory.pushState等を呼んでいる
}
}, { once: true });
...
Safariは、濫用防止のため、クリックやタップなどのユーザインタラクションに起因する場合のみhistory.pushState
が有効に機能する仕組みになっている。より厳密には、「ユーザインタラクションに起因しなくてもhistory.pushStateは呼び出され、履歴自体は追加されるが、追加された履歴はブラウザのbackボタンでスキップされる」という挙動になっている。
その制限をなんとか回避するため、ユーザが何らかの理由で画面内をクリックしたタイミングを狙って履歴をねじ込んでいるというわけである。
興味のある方は、デバッガを開いて当該箇所にブレークポイントを設置し、ページ内のどこかをクリックしてみると動きがわかる。window.history.length
がひとつ増える様子も観察できる。
Safariのデバッグツールって、難読化されたコードもこうやってフォーマットしてくれて、ブレークポイント設定しやすいんだよね。
また、Google Chromeの場合、もっと複雑な挙動をする。どんな場合にブラウザバック広告が出るのか、どんな条件で一度追加された履歴が無視されるのか、再現性がない。
History APIの目的外使用は以前から問題視されており、2018年の時点でこんな情報がある。
Google Chromeは「戻る」ボタンで戻れない悪質なウェブサイトを駆逐する予定 – GIGAZINE
Google Chromeはユーザーに歓迎されない履歴操作を行うページに対して密かにフラグを立て、Googleにその分析結果を送信します。最終的には悪質な履歴操作入りのページは完全にスキップされるようになるとのこと。
Google Chromeは「戻る」ボタンで戻れない悪質なウェブサイトを駆逐する予定 – GIGAZINE
その後、実際にどんな実装になったかはわからないが、サイトごとに「日頃の行い」をチェックしてHistory APIの使用を制限しているっぽく、Safariよりもさらに複雑なことをやっているような気配がする。
ブラウザUIへの信頼が破壊される
最近のWebは無節操だ。記事の続きを読もうとすれば前面広告が挟まり、スクロールしただけで画面が暗転して広告がふわりと浮き上がるし、上からバナーが降ってくる。それでも、それがページの中で起こっている限り「行儀の良くない挙動だな」「ずいぶん鬱陶しいサイトだな」という範囲にとどまる。
しかし、ブラウザバック広告/レコメンドは一線を越えている。明確にユーザの意図に反する動きをしている。
ブラウザUIだけは信頼できるものでなければならない。どんなに鬱陶しいページを開いていようと、戻るボタンを押したら「さっき見てたやつ」が表示されないといけないはずなのに、その想定が裏切られ、「全く見たこともないやつ」が表示されるのだ。
こういうことをするのはやめてほしい。