何が起きていたか
タイムラインの各投稿カードの下に出るはずの絵文字リアクションボタン(「😊 +」と、押された絵文字のカウント)が表示されない、という報告があった。
ブラウザの開発者ツールで中身を見ると、リアクションバーの入れ物そのものは存在していた。ただし中身が空で、高さだけが確保された状態になっていた。つまり「置き場所は用意されたが、そこにボタンを詰める処理が動いていない」状態だった。
仕組みのおさらい
リアクションバーは、サーバー側では空の入れ物だけを出力する設計にしている。中身はブラウザ側のJavaScriptが、リアクション数の取得APIと絵文字一覧の取得APIを呼んでから組み立てる。
これは表示速度のための設計で、リアクション数はよく変わるためサーバー側でキャッシュしにくく、後からブラウザが取りに行ったほうが都合が良い。
原因
問題は、そのJavaScriptを置いていた場所にあった。
リアクションバーのコンポーネントは「投稿カード1枚ごとに描画されるパーツ」である。そこにクライアントスクリプトを同梱していたため、スクリプトも投稿カードの数だけ出力される形になっていた。同じ処理が何度も走らないよう、スクリプトの先頭には「すでに初期化済みなら何もしない」というガードを入れてある。
ここに落とし穴があった。
タイムラインの投稿には、リアクションバーを持つ種類(つぶやき、ブログカードなど)と、持たない種類(ステータスカードなど)がある。ページを開いた直後に表示される数件がすべて「リアクションバーを持たない種類」だった場合、ページの中にスクリプトが1つも出力されない。
さらに悪いことに、その後スクロールして読み込まれる投稿は、取得したHTMLを解析してページに差し込む方式で追加される。ブラウザの仕様上、この方法で差し込まれた <script> タグは実行されない。
結果として「スクリプトのタグはHTML上に存在しているのに、一度も実行されない」という状態になり、後から追加された投稿のリアクションバーは永久に空のままになっていた。
最初の数件にたまたまつぶやきが含まれていれば動く、含まれていなければ動かない。再現条件が投稿の並び順に左右されるため、気づきにくい不具合だった。
直しかた
クライアントスクリプトと共通スタイルを、リアクションバー本体から切り離して独立したコンポーネントにした。そして、リアクションバーを表示する可能性があるページの </body> 直前に、そのコンポーネントを1回だけ置くようにした。
- リアクションバー本体 … 空の入れ物(div)を出力するだけ
- 新設したスクリプト用コンポーネント … 描画処理とスタイルを持ち、ページに必ず1回だけ出力される
これで、ページ上に投稿カードが1枚もなくても、スクリプトは必ず読み込まれる。後からスクロールで追加された投稿についても、既存の監視処理(追加された要素を検知してリアクションを読み込む仕組み)がそのまま働く。
配置したのは、タイムライン、個別投稿ページ、ブログ記事ページ、その他タイムライン形式で一覧を出しているページである。
副次的な効果
これまではリアクションバーの数だけ同じスクリプトがHTMLに埋め込まれていた。投稿を多く表示するページでは、同一内容のスクリプトが何十回も重複して転送されていたことになる。
1ページ1回の出力に変えたことで、HTMLの転送量が減った。無料プランの帯域を使い切らないよう気を配っている環境なので、これは地味に効く。
学び
「コンポーネント単位でスクリプトを同梱する」書き方は手軽だが、そのコンポーネントが1つも描画されない状況を考えていないと今回のような穴が空く。
ページ全体に対して1回だけ動けばよい初期化処理は、最初からページ側に置くのが安全である。特に、後からHTMLを差し込む無限スクロールのような仕組みと組み合わせる場合、差し込まれたスクリプトは実行されないという前提を忘れないようにしたい。