created_at
updated_at
tags
toc
headings

Web のセマンティクスにおける Push と Pull

Intro

筆者は、 Web のセマンティクスに対する実装の方針として、大きく Push 型の実装Pull 型の実装 があると考えている。

もっと言えば、それは実装方法という具体的な話よりも、開発者のセマンティクスに対する態度を表現することができる。

この話は「Push よりも Pull が良い」などと簡単に切り分けられる話ではない。

「自分は今 Push で実装しているのか、 Pull で実装しているのか」この観点を意識するかしないかによって、セマンティクスに対する視野が広くなり、その応用として、たとえば今自分が行っている実装が、将来の Web においてどのような互換性の問題を生じるかなどを想像できるようになるだろう。最近問題になる Ossification を、こうした視点の欠如の結果とみることもできる。

(本エントリでの Ossification は、一般に言われている Protocol Ossification よりも、もう少し広義に捉えていることに注意)

抽象的な話が続くため、なるべく具体例を交えて解説を試みる。

title タグとサイト名

身近な例として HTML の <title> を考えてみよう。

<title> にはその文書のタイトルが入る。これは仕様にも明らかだ。例えばこのエントリだとタイトルタグは以下のような実装が考えられる。

<title>Web のセマンティクスにおける Push と Pull</title>

同じく HTML を理解する各実装は、これを以下のように扱う。

  • ブラウザがタイトルバーやブックマークに表示する
  • 検索エンジンが検索結果として表示する
  • このページを引用/リンクした他のサイトが表示する
  • SNS で URL が投稿された際に表示する
  • etc.

このサイトの実装者(つまり筆者)は、このドキュメントを利用(消費)する各実装(クライアント)と、「文書のタイトルは <title> に含まれている」というルール(合意)に基づいて、情報を共有しているわけだ。

HTML を伝達し、パースして文字列を取り出す方法が プロトコルシンタックス であるとするならば、パースした結果 <title></title> の間に込められた開発者の意図と、それを汲み取る側の間にある合意こそを セマンティクス と捉えることができる。

ここで、情報提供の方向を考えてみよう。筆者は単にこうしたセマンティクスに基づいて文章を作成し、公開している。パブリックであるため、だれがアクセスしどう利用するかは利用規約以上に規定/制限していない。つまり「セマンティクスに基づいて作成し公開しているので、同じくセマンティクスに基づいて自由に消費して欲しい」という姿勢で実装していることになる。これを消費側の需要に委ねた Pull 型の実装 と呼ぶことにしよう。

あらゆるクライアントに対してコンテンツを提供する

しかし、見ればわかるが筆者が実際に本サイトで行っている実装はこうだ。

<title>Web のセマンティクスにおける Push と Pull | blog.jxck.io</title>

後ろにサイト名を補足している。本来の「独立した文書の表題」という観点でいえば、そこにサイト名は入らないだろう。しかし周知の通り、このように後ろにサイト名を入れる実装は、非常に一般的に行われている。

それはなぜだろうか?

理由の 1 つには、「純粋にタイトルだけが流通した場合に、どのサイトのものなのかがわかりにくい」という問題への対策があるだろう。特に SNS などでの流通を意識すると、それがどのサイトであるかを示した方がわかりやすい場合がある。他にも、サーチャビリティ/エゴサビリティの観点で、一意なサフィックスが共通して付与されていると便利な場面も想像できる。

仕様では特に「何を書いてはいけない」といった、内容に対するルールは無いため、今となってはこうしたタイトルの実装も一般的になり、かつ Web においては「それも含めてタイトルだ」という暗黙の合意も形成されつつあるように感じる。一般化により正当化されるのはよくあることなので、筆者も特にこの実装に反対する意図はない。

しかしこれは、本来の「タイトルを記述する」という <title> の役割を超え、それを 消費するクライアントの挙動に対して、コンテンツ提供側が最適化をした結果 と見ることができるだろう。

これを Push 型の実装 と呼ぶことにしよう。

特定のクライアントに対して最適化したコンテンツを提供する

本来 <title> だけの流通で不便があり、サイト名などを補完する方が良いというユースケースがあるのならば、それを仕様にすることで解決するのが標準化だ。この例では、サイト名を含めるための <site> タグを標準化し、提供側は以下のようにそれを導入するといった方法が考えられる。

<title>Web のセマンティクスにおける Push と Pull</title>
<site>blog.jxck.io</site>

消費側は、 "タイトル" と "サイト名" をそれぞれ取得できるため、 ${title} | ${site} などのように組み合わせて消費/流通してもよい。もちろん一行の文字列にせずとも、カード型の UI を作って表示しても良い。選択肢に対して何を選択するかは自由だ。

この <site> の標準化は、特定の実装に寄せる Push 型の実装 が必要だったユースケースを、標準化によって Pull 型の実装 に引き戻すことを意味する。

実際に <site> は標準化されておらず、 JSON-LD や OpenGraph などの仕様がある程度それらをカバーしている。しかし、それらを適切に提供しても、消費側での流通において <title> の利用が支配的であるために、多くのサイトが前述のように <title> の中にサイト名の補足を行っている。

こうして Push 型の実装がベストプラクティスのように普及すると、逆にいまさら <site> 相当を標準化しても手遅れだ。むしろ、それに対応していないサービスとの互換性を保つのが難しく、下手にやれば以下のような表示が発生しえる。

セマンティクスにおける Push と Pull | blog.jxck.io | blog.jxck.io

このように、「実世界の実装がある一定の方式で普及してしまった結果、標準ベースで変更を入れる余地がないまでに固着してしまった状態」も、広義には Ossification (硬直化) と呼ぶことができるだろう。

(再掲: ここで述べている Ossification は、一般的に言われている Protocol のそれよりも、広義の意味で使っている点に注意。)

もう少し角度を変えて、別の例を見てみよう。

srcset と Client-Hints

画像を表示する際に、デバイスの DPR を意識して出し分けを行う場合を考えてみよう。ここでは DPR が x1, x2, x4 のサポートを想定する。

まず <img>srcset 属性を使えば、両方の画像を以下のように出し分けられる。

<img srcset="img-300w.jpeg,
             img-600w.jpeg  2x,
             img-1200w.jpeg 4x"
        src="img-300w.jpeg">

一方、昨今 User-Agent ヘッダで話題になっている Client Hints の仕様は、もともとこの DPR の情報などを HTTP のリクエストヘッダで送りたいというモチベーションから始まった。

# response
Accept-CH: DPR

# request
DPR: 2

このヘッダを用いれば、ブラウザが送ってくる DPR に応じてサーバ側で画像を出し分けることができる。

// DPR ヘッダによって x1, x2, x3 が送られる
const dpr = req.headers.get("dpr");
if (dpr === 1) {
  res.send("img-300w.jpg");
} else if (dpr === 2) {
  res.send("img-600w.jpg");
} else if (dpr === 4) {
  res.send("img-1200w.jpg");
} else {
  // ??
}

すると、 HTML は以下のままで良い。

<!-- img タグ自体は 1 つだが、解像度に応じて画像は変わる -->
<img src="img.jpg">

さて、読者はどちらが好みだろうか?

サイト全体の <img> タグ実装を余すところなく直すのは一般的に大変なので、サーバ設定一発で対応できそうなヘッダ方式の実装は、導入が楽で好まれがちだ。実際に、そうした機能を提供する画像系 CDN サービスは複数存在し、パフォーマンス改善の文脈で導入されている。

ところで、 デバイス自体の DPR が 2 であることは、サーバが x2 の画像を返すべきことを本当に意味するだろうか?

仕様上はそのまま「DPR が 2 である」ことを意味する以上の定義はない。それを踏まえてどの画像を返すかをサーバが決めているだけだ、これも Push 型の実装ととらえることができる。

例えば、 DPR が 4 の端末だが、月末で既にパケ死していて、帯域が制限されていたら、それでも x4 画像が表示されることをユーザは望むだろうか? これを明示的に示すために、同じく Client Hints に提案された Save-Data が一緒に送られてきたら、どっちをとるべきだろうか?

# 何を返す?
DPR: 4
Save-Data: on

他にも、仮に DPR が 4 だが実際は Width 300 程度の超高精細時計型ディスプレイがあった場合、そこに表示される DPR 2 と 4 の差は人間に認識できるだろうか? それでも x4 を返すべきだろうか?

# 何を返す?
DPR: 4
Width: 300

クライアントからありとあらゆる情報を収集し、それを条件に分岐を書くことで「クライアントが求める適切なものはこれだ」と思うものを想像で返す実装は可能だ。しかし、それはどこまでいっても提供側の「思い込み」の可能性がある。もっと酷く言えば、この画像を表示させたいという「提供側の都合を押し付けている」だけとも言えるかもしれない。

クライアントは多様な実装があり、それを使っているユーザにも多様な「条件」や「状態」や「都合」があるため、その全てのケースをカバーする実装がサーバ側でできるとは限らない。正確に言えば どこまでやっても開発者の想像の域を出ない

一方 <img> での実装を見てみよう。

<img srcset="img-300w.jpeg,
             img-600w.jpeg 2x,
             img-1200w.jpeg 4x"
        src="img-300w.jpeg">

この実装は「サーバには x1, x2, x4 の画像の用意がある」以上のことは何も言ってない。クライアントは自分の DPR や Device Width にあわせて適切な画像を選んでもよいし、データセーバーが有効だからとにかく一番小さい画像を取得しても良いし、サーチのためにクロールしているから一番高精細な画像を取得しても良い。アーカイブのために全部取得しても良い。

OS やブラウザは、今クライアントがどういう状況に置かれているかを、サーバとは比較にならないくらい高い解像度で理解している。 OS やブラウザは、時代の要望が変わればそれを取り込んで更新されていき、クライアントはニーズに応じてカスタマイズもできる。取得すべき最適な画像は、サーバよりもクライアントの方が適切に選択できる場合が多いだろう。

この実装も、提供側はあくまで自分たちが提供できる選択肢を明示し、消費する方法は消費側にゆだねているのだ。これは Pull 型の実装と捉えることができる。

では、なぜ Push / Pull 両方が標準でサポートされているのだろうか? 標準 API を正しく用いたら Pull になるというわけではないのか?

ここでのすれ違いは、解釈の帰結先がずれていることによって起こっている。例えば、"DPR: 2" はまぎれもなくデバイスの DPR を伝えている。その情報を取得すること自体は Pull だ。問題は「DPR が 2」であることを「だから x2 の画像を送る」と帰結させてしまっている部分にある。クライアントは最初からそんなこと言ってないのだ。

最初に述べたように、 Push / Pull とは実装の態度だ。仕様は基本的に Pull で考えられており、「特定のユースケースや特定の実装に向けて Push で使われる」だけだ。

とはいえ、 DPR の場合は、最初から画像を出し分けるというユースケースのために Iliya が提案したという経緯があり、当時「それって srcset の方が良いんじゃないの?」という話をしたところ、「こちらの方が上手く問題を解決できるケースがある」という話をされたのを覚えている。おそらく、前述のようにサイトに手を入れずに解決できなければ、スケール(普及)しないという実態に合わせた現実解だったんだろう。そして現在その方法は成果を出しており、世のページ表示が早くなるために貢献しているため、それは否定されることではない。

このように Push と Pull は良い悪いで一刀両断できる話ではないのだ。

ただし、そのまま野放しにすると、コンテンツ提供者がそうした Push による実装をより正確にしたいがために、さまざまな情報を追加でクライアントに送らせたがり、それがエントロピーを上げ、 Fingerprint ベクタになり、また新たな問題を生む可能性もある。 Push の精度を上げるのは限界があり、 Pull の場合はその情報をクライアント内に閉じることができるという点は、常に意識しておきたい。

User-Agent と嘘

Push によって Ossification が発生すると、クライアントが嘘をつかないといけなくなることがある。

その典型例が User-Agent だ。

最古のブラウザの 1 つである NCSA が作った Mosaic は以下の User-Agent を送っていた。

NCSA_Mosaic/2.0 (Windows 3.1)

これに対抗し Mosaic + Killer = Mozilla が作った Netscape はこうだった。

Mozilla/1.0 (Win3.1)

この後、 Microsoft が IE をリリースするにあたって、本来ならば以下のようにできただろう。

MSIE/1.0 (Windows 3.1)

ところが当時非常に重宝されていた <frame> タグは、 Mozilla Netscape は対応していたが、 Mosaic は対応していなかった。そこで Web 開発者は、 MozillaMosaic かで分岐し <frame> の出し分けをしていた。当時はメジャーなブラウザが 2 つしかなかったので、結果以下のような実装が横行した。

function frame_supported(user_agent) {
  if (user_agent.includes("Mozilla")) {
    return true
  } else {
    return false
  }
}

後発の IE は <frame> をサポートしているにもかかわらず、 UA に Mozilla が含まれないために、このような実装で弾かれてしまう。そこで、この分岐に入るように嘘をつくしかなかった。結果、以下のような UA 文字列を実装することになる。

Mozilla/1.22 (compatible; MSIE 2.0; Windows 95)

以降同じような歴史が積み重なって、今の長い UA ができ上がっていく。

Web 開発者はこの現状を「これだからブラウザは~」というマクロな主語で非難することがよくあるが、ブラウザベンダがやりたくてやっているわけではなく、同じ大きさの主語で言えばむしろ悪いのは「Web 開発者」と言える。しかし、「ブラウザは」と非難している開発者は「Web 開発者のせいで」というマクロなブーメランが自分に向けられた場合に「自分のせいではない」というミクロな主語で回避するために、当事者意識をもってこの問題を捉えている人は少ない。

元凶は何かというと、「User-Agent はあくまで User Agent を表しているだけ」なのに対し、開発者は「UA がこうであれば <frame> に対応している/していない」という解釈を入れ、それを Push で実装するために使ってしまっていることだ。

Progressive Enhancement という考え方が広がって以降は、特定機能の有無は Feature Detection によって行い、 Fallback や Polyfill の提供によってカバーされるのが定石となったが、当時はまだ Web の黎明期でそうしたプラクティスが無く、現実的にこの方法しかなかったのも事実だ。結局誰が悪いわけでもない。

昨今では、積み重なった嘘の数がエントロピーを上げ、 Fingerprint ベクタになり、これ以上悪化しないように改善されようとしている。しかし、十分に Ossification されているため、かなりの痛みを伴っているのは、開発者も痛感しているだろう。

最初から Pull で実装されていれば、 User-Agent は今の Sec-CH-UA と同じくらいだったかもしれないし、そうなった原因は開発者による安易な Push 実装だということに開発者自身が気づかなければ、ブラウザは Sec-CH-UA でも嘘をつくことになるかもしれない。むしろ Safari は 実装するとしたらおそらく最初から嘘を付くだろう という話をしている。

Wi-Fi かどうか

Network Infromation API は、接続しているネットワークが Wi-Fi なのか Cellular なのかを取れるようにしようとしているが、この仕様を見て「Wi-Fi だったら大きいデータを送り、 Cellular だったら小さいデータを送る」というユースケースが紹介されることがある。

if (navigator.connection.type === "wifi") {
  // 大きなデータをダウンロード
} else {
  // 小さいデータをダウンロード
}

さて、 回線が Wi-Fi であることはネットワークが潤沢であることを、未来永劫意味するだろうか?

確かに今はキャリアの回線よりも Wi-Fi の方が太く、速く、定額な場合が多いかもしれない。しかし、それが将来でも同じと保証できるだろうか? 5G 以降は速度やプランの状況も変わりつつあり、キャリア回線が太くて固定で常用され、 Wi-Fi はたまに駅で拾って勝手につながるくせに遅いだけのものになるような将来に、絶対にならないという保証はあるだろうか?上記のコードはそうした未来を想像してない。

仮に、そんな未来が来たときに、何が起こるか考えてみよう。

キャリアよりも Wi-Fi を優先する Push 型のコードは更新されないが、実際にユーザはキャリア回線を優先したい。となれば、そこではもう Ossification が起こっているのだ。ここで互換性を保ったまま、その未来に対応する方法の 1 つは、やはり ブラウザが嘘を付くこと だ。

つまり「Wi-Fi に繋がっているときは "cellular" を返し、キャリア回線の場合は "wifi" と返す」ように嘘をつき、古い時代の常識で固定化されてしまった実装を騙すしかない。それを馬鹿な話と思うかも知れないが、 User-Agent ヘッダが嘘まみれなのも同じくらい馬鹿な話だ。そして、そうやって互換を守るために嘘を付くブラウザを、自分たちはまるでなんの罪もないかのように非難する Web 開発者の姿も容易に想像できる。

ではなぜこんな仕様が提案されているかというと、そこに需要があるからだろう。筆者はこの提案にあまり詳しくないが、まあ 現時点において は Wi-Fi とキャリア回線の使い分けは、特にモバイルアプリでは当たり前の機能なので、それをそのまま Web に持ち込めばこうなるだろうとは思う。

では、仮にこれを Pull 型で考えると何があるだろうか?

問題は「Wi-Fi に繋がっている」ことを「大きいデータを送信して良い」に解釈していることだ。もしネットワークの切り替えがダウンロードするファイルのサイズに起因する問題なら、例えばこれからダウンロードしようとしているファイルのサイズを明示するアノテーションを用意し、それをダウンロードするかしないかの判断はクライアントに委ねるといった方法が考えられる。

選択肢がクライアントにあるということは、ユーザがブラウザや OS に設定した制限や、端末から取得できるキャリアやプランなど様々な情報を元に、どう挙動するかを選択できる。

Pull 型の実装の本質は、「クライアント実装に対して選択肢を与える」ことも意味する。逆を言えば、 Push 側の実装とは「こう挙動しろ」という命令をクライアントにしているに他ならず、その命令が時代とそぐわなくなっても更新されない実装が残る状態が、先程から述べている広義の Ossification とみることができるだろう。

Promise の throw

特定の意味を持つ API を、その意味を無視して用いる場合も問題が発生し得る。

言語において、 例外を投げることは「なんらかの例外的なことが起こった」という意図を示すために使われている。それが適切に処理されることは、正常系の処理を継続するために必要不可欠なことだ。

その価値は、 UI においては一層重要になる。Node.js でのプロセスは処理されなければプロセスが落ちるためすぐにわかるが、ブラウザは例外が捕捉されなかっただけでページをエラーにしたり、タブを落としたりすることはできない。また、ブラウザでどんなに例外が上がろうと、開発者は手元の /etc/error.log ファイルで監視できるわけでもなく、なんらかの方法で積極的に収集する必要がある。そうしたニーズに敏感なブラウザという環境は、 DevTools との Integration や CSP, Reporting などの標準を整備しつつあり、エコシステム側でもブラウザで起こっていることを知るためのサービスやツールが育ちつつある。

これらは、ブラウザで投げられる例外や、発生するエラーは、開発者が知る必要がある何かしらの問題を表し、それを Report し収集することは問題を解決するための行為だという合意のもとに成り立っている。

もし、ここで例外の throw を例外の発生以外の用途で利用したり、レポートの送信を本来の目的以外の方法で利用することが一般的になれば、その合意が崩れることになりかねない。その歪はまた、「エラーでしか投げられない例外」、「本当に問題があったときにしか送れないレポート」、「throw をフックするデバッグ時に無視しても良い例外」などといったいびつな仕様の標準化か、「ブラウザの嘘」などによってカバーされるかもしれない。

「例外を throw すれば本来行けないあの行に遷移する」ことを逆手に取り「行きたい行に遷移させるために例外を投げる」というのは、例外というセマンティクスを完全に無視し、その挙動だけに着目した Push の発想だ。コードのフローが変わり、使いようによってはコードがスッキリするといった発想の裏には「例外を例外的状況以外に使うことの未来への弊害」に対する想像力が欠如しているように思える。ここまでに紹介してきた歴史的な負債を築いてきた人のメンタルモデルとおそらく同じなのだろう。

本来、大域脱出によって改善するユースケースがあるなら、それに適したメンタルモデルを標準化し、それを用いて Pull で実装するのが、「問題」の解決だ。しかし、特に近年のフロントエンドは、標準化プロセスの重さを避けるために、ツールによる変換(e.g. build/compile/transpile) などによって「手元で課題を解決する」文化が強く根付いた。それは良い面もある。しかし、なまじ手元で「課題」が解決できてしまうために、根本にある「問題」への取り組み自体がおろそかになる場面も多い。

非同期コードをきれいにするための提案の PoC として例外を上げる実装はあるかもしれないが、それが実際にどこかにデプロイされてしまえば、 Web はその互換性を維持する方を選ぶ。デプロイされてからでは遅いのだ。ここで手を抜くのがブラウザ側に非難される「ミクロな開発者」の発想であり、それに合わせてブラウザはまた歪んだワークアラウンドを提供することになる。そのいびつな現状をみた後世のエンジニアが非難するのもまた「マクロなブラウザ」なのかもしれない。

「セマンティクス」をどう理解するか

よく初心者向けの解説で

GET はパラメータが URL に入り、ブラウザの履歴やサーバのアクセスログなどに残ってしまう。だからログインなどはパスワードが body に入る POST を使うべき

といった解説を目にすることがあるだろう。

この考え方自体は、 GET と POST がどういうセマンティクスを持っているかではなく、それがどう消費され挙動をしている実装があるかに着目した見方だ。つまり、「GET だと残ってしまうから POST を使う」という考え方自体も、Push の発想だ。

では、この解説をログインにフォーカスして Pull の視点からするならば

ログインは何かを取得するのではなく、データをサーバに送信するのが目的なので <form> の method は POST を使うべき。そこで用途の違う GET を用いると意図しないことが起こる可能性がある

という解説になってしまう。これは「GET には GET のセマンティクスが、 POST には POST のセマンティクスがあり、正しく使い分けろ」という抽象的な話以外はしてない。それは、初心者に向けた解説としてはあまりにも投げやりすぎるので、前述のように実利/実害ベースで教えるのは、理にかなってはいるだろう。 DELETE ではなく GET で削除してはいけないのも、「Bot がクロール時に間違って消してしまうから」が本質ではないが、そう言われた方が理解しやすい。

一方で、そういう教育の延長で、その裏にある「GET には GET のセマンティクスが、 POST には POST のセマンティクスがあり」の部分に向き合う機会がなく、セマンティクス実装の基本を「実利/実害や挙動に呼応する Push ベース」で考えてしまう、悪く言えば「Push ベースでしか考えられない」開発者がいるのも事実だと思う。このとき Push 型で考えているという事実自体に、気づくきっかけも、教えてくれる人も、実際は少ないのかもしれない。

ここに、 Web に対する理解の解像度の一つの壁があると筆者は感じる。

非機能要件の最適化と Push

ここでいう「非機能要件の最適化」とは、 Accessibility や Performance や Security について、それらを向上するために最適化を行うことだ。これらは本質的に、機能要件を満たし挙動しているものを、非機能要件を向上するために、特定の実装/挙動に対して Push していく行為とみることができる。

これが Push は必ず悪で Pull が常に善ではない理由の一つだ。つまり Pull で作られたものが、現実的に使いやすいとは限らない のだ。

その視点からいくつか見てみよう。

a11y

画像には alt という属性があり、ここには「代替表現」というセマンティクスがある。

もともとは、例えば画像サーバが落ちており画像が取得できなかった場合に、その属性を表示することで何が表示されるべきだったかを示す役割が主流だった。しかし、スクリーンリーダーが一般的になって以降は、読み上げられない画像の代わりに読み上げる目的が注目されるようになった。以降「読み上げたときに不自然ではない」オーサリングがなされることも増えたように思う。

読み上げるための別の属性が存在すれば、そこに読み上げのための文字(つまりセリフ)を含むのは Pull だが、汎用的な代替表現を読み上げに最適化するのは、スクリーンリーダーに対する Push の実装といえる。読み上げることに最適化してオーサリングされた alt が、別のシチュエーションで alt を消費する実装にとって適切とは限らないからだ。

特に、 a11y の文脈でよく言われる「読み上げなくて良い画像の alt は空で良い」というのも Push の典型的な一例だ。そこに画像がある以上、それがアイコンであろうと罫線であろうと存在することに変わりはない。読み上げると邪魔だから不要というのは、他の消費者(クライアント)に対する配慮を欠いている点で、それをアクセシブルと呼んで良いのか個人的には非常に疑問だ。特に、最近議論されている COGA のように、弱視以外の理由でサポートが必要な人々(e.g. 認知/認識に関する障害など)にとって、 Screen Readerbility に最適化された alt がそのまま有効なのかについては、議論の余地があるだろう。

本来それが contents ではなく style であるから不要、という理由であれば CSS でオーサリングすべきだし、読み上げと代替は別と考えるなら、適切な他の属性(e.g. Aria)を使うことで全てを Pull のまま実装することもできなくはない。しかし、 alt 以外にも、様々な属性や実装方法に対して、スクリーンリーダーの挙動を起点に考えるプラクティスは多くある。仮にコンテンツが Pull で実装されているにも関わらず、それがスクリーンリーダーにうまく消費されないのだとしたら、治すべきは本来「標準仕様」か「スクリーンリーダー」であるはずだ。しかし、開発者でシェアされる多くのプラクティスやガイドラインは、コンテンツガイドライン、つまりコンテンツ側を特定のスクリーンリーダー実装に対し Push にするためのものが多い。

ここに accessibility と screen readerbility の境を見出すと、 a11y は本来 Web のセマンティクスを正しく理解し、正しく実装する以上のものではない。そうならないならば、悪いのは仕様か実装であるはずだし、その世界においては a11y という明示的なラベルは本来不要だ。ただ、現実的にそうしたコンテンツを「正しくかつ適切に消費できるスクリーンリーダーの存在」は前提にできないため、コンテンツが歩み寄るしかない。つまり現状は Push するしかなく、その Push するベクトルに対して a11y というラベルがついている。

ただし、現状多くのサイトのコンテンツは、正しく Pull で実装されているわけでも、それによって Accessible なわけでもないもの事実だ。

このラベルがないと不利益を被る人がいることは、それだけ Web が力不足だということの裏返しとも言えるかもしれない。

Performance

Web のパフォーマンス改善の文脈も、基本は「ブラウザで速く表示される」というのが中心にある。

その施策はやはりブラウザの挙動に Push するものがほとんどだ。そのうえ、現状クライアントの実装としてブラウザは支配的で、どのブラウザもだいたい同じように挙動するために、プラクティスがシェアしやすいという側面もある。

つまり、ブラウザにおけるパフォーマンス改善は、その視点で見れば「どれだけブラウザに Push できているか」を追い求めていると捉えることができる。CWV や Lighthouse などは、それがどの程度達成できているのかを、スコアリングで可視化するために作られたものだと言える。

それらを用いた改善によって、現に速くなっているのだから問題は無いように思える。しかし、無いとも言い切れない。

例えば、 HTTP/1.1 はリクエストを多重化できないため、コネクションを 1 host につき 6 本張るという挙動が一般的だった。そこで、「6 つ以上のリソースがあるならドメインを分ければさらに 6 本張って並列取得できる」というプラクティスが、 "ドメインシャーディング" として普及した。これが、挙動に対する Push の実装だったのは言うまでも無いだろう。

似たようなプラクティスは多々有り、当時パフォーマンス改善と呼ばれていたものの実態は、そうやって HTTP/1.1 の挙動に合わせてコンテンツを Push で実装することそのものでもあった。

後に HTTP/1.1 自体を改善した HTTP/2 は、 1 本の接続にリクエストを多重化するにとどまらず、 RTT を減らし、 Window Size も上がったところを維持でき、バイナリ化によってパースのコストを下げ、ヘッダ圧縮によって転送量も減らした。結果、ブラウザは「無駄に多くの接続を貼らなくても 1 本を効率良く使いまわせる」 HTTP/2 を実装し、その接続は 1/6 になった。

それがわかっていれば、 HTTP/1.1 に最適化されたままのコンテンツを、そのまま HTTP/2 でデプロイしたところで、パフォーマンスが何倍にも速くなるはずがないことは、少し考えればわかるだろう。下手すれば、単純に接続が 1/6 になったせいで、遅くなるコンテンツも多くあるはずだ。逆に HTTP/1.1 に対する特別な Push を行わずに、ごくごくシンプルに作られていたコンテンツの方が、単純な置き換えでパフォーマンスが向上する可能性もある。ちょっと試して雑に h2 と h1 を比較計測した結果「こんなプロトコルは使い物にならない」という批判をするのは自由だが、それは単に「自分にはよく理解出来ずうまく使いこなせなかった」を曲解しているだけかもしれない。

「プラクティス」とは、その本質が理解できてない人にも、「こうすれば良い」というタスクを見いだせる点で便利ではあるが、自分が「何に対して Push しているのか」を自覚できていなければ、前提が変わったときに自分が本当に評価すべきものを見失うという危険性もはらんでいる。

さらに、その「プラクティス」を可視化する「スコアリング」は、気づかぬうちに「スコアをハックすること」にすり替わる点が恐ろしくもある。過度にスコアを追い求めた Push を重ねれば「そのスコアリングに最適化した結果」は比較的容易に得られるが、どんなスコアリングも所詮は補助ツールであり、実際にそれで非機能要件が改善できているかは別の話だ。スコアリングツールに Push を重ねただけでは、スコアが良くても遅いサイトや、スコアリング方法が変わったらグリーンだったものがレッドになるようなサイトが生まれないとも限らない。

もちろん、Lighthouse や WebPageTest などは、簡単にハックできるほど本質からずれてないし、パッと見ではわかりづらい問題を可視化する上では有用だ。時には「Push で実装されているが Pull に直した方が良い」ということを教えてくれることもあるだろう。

一方、 Performance は実装と密結合であるがゆえに、「仕様がどうであれば実装が効率的になるか」が見えやすいという側面もある。大抵は、サイズを下げる、遅延する、投機実行する、回数を減らす、というパターンに落ち着き、それらは続々と仕様に反映されて Brotli, LazyLoad, Resource Hints, WebBundle, HTTP3 などの仕様として提案され、 Pull 型の実装を可能にしている。

ところが、現実にパフォーマンスの問題を抱えているサイトの大半は、なにかに対して Push が足りてないというよりは、「普通のことが普通にできていない」ため、「遅くなっている」だけで、普通のことができていれば、「遅くはない」状態にできることが大半だ。そこからさらに Push によって「パフォーマンスチューニング」の域に達した議論ができるサイトは、体感では非常に少ない。

User Safety

脆弱性は、挙動に対する Push が不利益を被る状態であり、その対策は実装と攻撃手法に対して Push 型の実装を行う必要があり、それが 必須 だという点で他の非機能要件ともまた扱いが違う。

例えば、単純に <form> を用いて投稿画面を作れば、それは Pull 型の実装だ。しかし、それでは他のサイトからのリクエストと見分けがつかないため CSRF が発生し、その挙動に対して One Time Token を仕込むという Push 型の対策を行う。できてないところがあれば、問答無用で実装を施すし、それを見つけた人には報奨金を支払う企業も増えてきた。

Security は、最も Push に余念の無い分野だと言える。

One Time Token を仕込むという行為自体が、将来的な問題を引き起こす可能性は筆者にも想像し難いが、本来であれば「そんなことしなくても安全である」べきとも考えられる。そして、近年「そんなことしなくても良い(safe by default)」状態に対して一番敏感なのもまたセキュリティだろう。

特に Web でカバーされるユースケースが増えるに従って「ユーザの安全」という課題の比重が高くなる。さらに昨今の「ユーザの安全」は「セキュリティ」だけでなく「プライバシー」も含まれるようになり、「デフォルトで安全である」ことがブラウザにも求められるようになった。

例えば、 <form> に One Time Token を能動的に仕込まずとも、今のブラウザは <form> からの POST には Origin ヘッダを付与するし(ちなみに Origin は出自を示すヘッダであるため、出自が意図しなかったら弾く実装は Push ではなく Pull だと解釈できる)、 SameSite=Lax な Cookie は攻撃サイトからのリクエストに自動で付与されない。これらを用いれば CSRF を One Time Token 無しに Pull で防ぐことも可能だろう。(そうやって Pull で実装できても、セキュリティエンジニアにとっては任意のヘッダが仕込める拡張の脆弱性が脳裏に焼き付いているので、値が予想できるヘッダを信用せずに One Time Token を送り続けるかもしれないが)

今最も議論の的になる 3rd Party Cookie は事情が複雑だ。 Cookie はセッションを維持するという建前がありそうで、実際には「クライアントが保存し、リクエストに付与する」という、挙動の側面しか標準化されていないため、 3rd Party Cookie による Tracking は、 Push のようで Pull だったと見ることもできるだろう。だから、それを防ぐには法や規制による対策しかなかったが、それが機能しないために、ブロックすることに踏み切ることになった。代わりに、それまで行われていたユースケースをカバーするために、代替の API が提案され、それらは個別のユースケースを Pull で実装できるように策定が進められている。

Pull で実装できれば、クライアントに選択肢があるため、例えばユーザに関する情報をどの粒度で、どういう頻度で、誰に対して提供するかなどをクライアントが調整できるようになる。これにより 3rd Party Cookie 時代に得られていた情報に比べれば自由度が減るために、他の挙動に対して Push しまくってなんとかしようとする。それが Fingerprint であり、対策として User-Agent を固定したり、 IP を隠したりすることで Push の限界を下げる調整も、「ユーザの安全のため」に行われている。

今行われている Security / Safety 系 API の歴史的な移行は、 Push で行われていた負債を、 API の Pull 化によって精算するプロセスと捉えることもできそうだ。

Push か Pull かの自覚

「Push は害悪であり、常に Pull で考えるべき」と言い切れればそんなに簡単な話は無いが、残念ながら現在の Web はそんなに単純ではない。

全てを完璧にカバーする仕様があるわけでも、それを完璧に実装したブラウザがわるわけでも、それらを完璧に理解した開発者がいるわけでもない。仮に今完璧に見えるものも、来年には不完全なものに変わる可能性もある。

時代が変わり、ニーズがユースケースを生み、課題を解決するために Push が行われ、問題を解決するために標準化によって Pull に引き戻そうとしたころには、 Ossification しているか、開発者の関心が別の何かに移っている。結果 Push でしか実装できないものや、そうしないと使いやすいものにならないケースは、無視できないほどに多い。

人手が足らないところ、コストが見合わないところ、互換性、理解と周知、様々な問題と折り合いをつけて、今の Web は成り立っており、筆者にしてもそのエコシステムの一部に過ぎない。

そのエコシステムの一部として、そして一人の Web 開発者の矜持として、「今自分が Push をしているのか、 Pull をしているのか」は、意識し続けていきたいと思う。