スマホで開いたブログが10秒固まっていた話と、原因が過去の自分だった件

iPhone で自分のブログを開いたら10秒以上白画面が続きました。Cloudflare Fonts や Bot Fight Mode、Astro の client:idle を順に疑っていったら、最後に残った10秒の正体は、過去の自分が書いた1行の CSS でした。

公開日
読了まで
6 分

/ 6 分

目次

久々に iPhone で自分のブログを開いたら、白い画面が10秒以上動きませんでした。

PageSpeed Insights1 でモバイルのスコアを測ったら 53 点。最初に原因と考えたのは Cloudflare の自動最適化です。Cloudflare Fonts も Bot Fight Mode も、ダッシュボードのトグルを押しただけで、中身は一度も見ていません。

設定を解除してみると、たしかに遅さの一因ではあります。ただ、消したあとも白画面は10秒のまま動きません。最後まで残った10秒の正体は、Cloudflare でも Astro でもなく、過去の自分が書いた1行の CSS でした。

最終的に PageSpeed スコアは 97、FCP2 は 8.0 秒から 1.1 秒になりました。この記事では、検証の過程で学んだことと、次に同じ症状が出たときのための調査手順をまとめます。

まず Cloudflare の設定を確認する

PageSpeed Insights のレポートを開くと、最初に出てくるのは「レンダリングをブロックしているリクエスト」、つまり Google Fonts の Noto Sans JP です。CSS のサイズは 119KB あり、1,530ms 間ブロックしています。フォントだろうな…と思いつつ、HTML 自体のサイズも測ってみます。

curl -s https://suntory-n-water.com/ | wc -c
# → 399,015 バイト(約 399 KB)

399 KB もあります。技術ブログのトップページは普通 20〜50 KB なので、明らかにおかしいです。中を覗いてみると、1つの <style> ブロックに 374 KB の @font-face 宣言が詰め込まれていました。

Style block 1: 373,928 bytes
  Preview: @font-face {font-family:Noto Sans JP; ...unicode-range: U+8b2d,...

Cloudflare Fonts の挙動でした。Google Fonts の CSS を外部リクエストなしで配信するために、@font-face 宣言を全部 HTML にインライン化するしくみです。Noto Sans JP は日本語対応で Unicode-range のサブセットが数百個あり、展開するとこのサイズになります。「速くなる機能」が、日本語フォントだと逆に肥大化していました。

Cloudflare Fonts を無効化すると、HTML は 99 KB まで縮みます。Google Fonts そのものも <link> ごと削除し、本文は system-ui, ヒラギノ角ゴ ProN, Yu Gothic, sans-serif のスタックに切り替えました。コードブロック用に自己ホストしていた PlemolJP35Console(1.1 MB)も外し、Menlo, SF Mono などのシステム等幅フォントに変更します。

ここまでやっても、iPhone の白画面は消えていません。

iOS Safari で13秒の空白がある

ローカルや Mac のブラウザでは速いのに、iPhone だけが遅いです。デスクトップの DevTools には症状が出ないので、iPhone を Mac に USB 接続して、Safari の Web インスペクタで HAR3 を取得します。

並べてみると、リクエストが2段階で読み込まれていました。

      0ms  HTML・CSS・React本体(全部ここで完了)
    700ms  Header.js
  -----  ← 13秒の空白 -----
  13,032ms  jsx-runtime.js, lucide-icons.js, Header(以下多数)

700ms で全リソースのダウンロードは終わっているのに、13 秒後に再度読み込まれます。空白の13秒には、原因が2つ重なっていました。

client:idle と requestIdleCallback の相性

第 2 グループに並んでいたのは、すべて Astro の client:idle を付けていたコンポーネントの JS です。

client:idle はブラウザのアイドル状態を検知して JS を遅延実行するしくみで、内部で requestIdleCallback を使います。便利そうな名前ですが、Safari はこの API をデフォルト非対応で、機能フラグの裏に隠れています。
Astro は非対応ブラウザ向けに setTimeout でフォールバックします。ところが iOS のバッテリー管理や省電力制御は、この setTimeout を平気で 13 秒以上ずらしてきます。「アイドルになったら実行する」が「電力的に都合がよくなったら実行する」になり、iOS ではその都合がなかなか来てくれません。

client:idle を全部 client:visible に変えます。IntersectionObserver ベースで、要素が画面に入った時点で即座にハイドレーションされ、電力管理の影響を受けません。

Bot Fight Mode のトグルが反映されていなかった

それでも window.load イベントだけは 12 秒後にしか発生しません。HAR をさらに眺めて、見慣れないリクエストに気付きました。

12,662ms  /cdn-cgi/challenge-platform/h/b/scripts/jsd/1eec42285

Cloudflare のbot検出スクリプト(JavaScript Detection)が 12.6 秒かかって、window.load をブロックしていました。

ダッシュボードに戻ってみると、Bot Fight Mode のトグルそのものはたしかにオフになっています。でも「JS 検出」の項目だけがオンのまま残っていました。コミュニティでも報告されているとおり、ダッシュボードのトグルだけでは JS Detection が消えないバグです。

API 経由で無効化する必要がありそうです。

curl -s https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/bot_management \
  -X PUT \
  -H "Authorization: Bearer {API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"enable_js": false, "fight_mode": false}'

これで window.load は 383ms まで縮みます。ところが、 iPhone ではまだ白画面が10秒続いていました。

残った10秒の正体は、過去の自分が書いた一行だった

スピナーは2秒で止まっていて、load イベントも発生しています。ネットワーク的にもアセット的にもページは完成しているはずなのに、画面は真っ白のままです。10秒ほど経ってから、コンテンツが一気に現れます。

「GPU の計算が終わるまで、ブラウザがペイントを保留している」状態です。今度はネットワークでも JS でもなく、描画の問題でした。

src/pages/index.astro のヒーローセクションに、ぼんやりした光のような装飾を入れている部分がありました。

<div class="... blur-[110px] animate-float">
  <!-- 320×320px に半径 110px のぼかし -->
  <div class="... blur-[90px] animate-float">
    <!-- 192×192px に半径 90px のぼかし -->
  </div>
</div>

いつ書いたか思い出せないくらい昔、見た目を整えるために自分で入れた一行です。

filter: blur(110px) は、半径が大きいほど参照するピクセル数が増え、計算量が急激に膨らむ処理です。Mac の GPU では一瞬で終わるため、Mac の Safari でも iOS シミュレータでも気付けません。モバイル GPU だと数秒〜十数秒かかります。そしてこの計算が終わるまで、ブラウザは一切ペイントを始めません。

要素を削除すると、白画面が消えました。

遅さの正体は、過去の自分が見た目のために置いた1行の CSS でした。

次に遅くなったときのための調査手順

今回の反省は、よしなにやってくれる機能ほど、しくみと影響を一度は自分の目で見ておくべきだった、ということです。Cloudflare Fonts も Bot Fight Mode も機能としては合理的で、日本語フォントや iOS Safari という自分の条件で噛み合わなかっただけです。そして、新しく入れた機能に目を向ける前に、過去に自分が書いたコードも同じくらい見直す必要があります。

次に同じ症状が出たときのために、自分用のたどり方も残しておきます。

まず HTML サイズを測ります。

curl -s https://suntory-n-water.com/ | wc -c

100 KB 前後ならまず正常です。3〜400 KB あれば CDN が何かをインライン化している可能性が高いと言えます。次に、注入されているスクリプトの種類を確認します。

curl -s https://suntory-n-water.com/ | grep -E "challenge-platform|cf-rocket|Cloudflare Fonts"

何も出なければ問題ありません。challenge-platform が出てきたら bot 検出 JS が動いています。ここまでで HTML 側の問題はだいたい潰せるはずです。

ここから先は実機を見るしかありません。iPhone の「設定 → Safari → 詳細 → Web インスペクタ」をオンにして USB でつなぎ、Mac の Safari の開発メニューから iPhone のページを選んで HAR をエクスポートします。
時系列で並べたとき、リクエストが2つのグループに分かれていたら、その間に何かがブロックしています。原因は状況によって変わりますが、分断さえ見つかれば絞り込みは時間の問題です。

まとめ

  • Cloudflare Fonts は Google Fonts の @font-face を HTML にインライン化するしくみで、Noto Sans JP のように Unicode-range サブセットが多いフォントでは HTML が 374 KB まで膨らむ
  • Astro の client:idle は Safari で setTimeout フォールバックになり、iOS の電力管理で 13 秒以上ずれることがある。client:visible に切り替えると IntersectionObserver ベースになり電力管理の影響を受けない
  • Bot Fight Mode はダッシュボードのトグルだけでは JS Detection が消えないことがあり、API 経由で enable_js: false を送る必要があった
  • モバイル GPU では filter: blur(110px) のような半径の大きいぼかしが描画を十数秒ブロックする。Mac の Safari でも iOS シミュレータでも気付けない
  • PageSpeed 53 → 97、FCP2 8.0 秒 → 1.1 秒、LCP4 8.9 秒 → 1.3 秒。数字よりも「入れた機能は中身を見ておく」「過去の自分のコードも見直す」という当たり前のことを得た1日だった

参考

Cloudflare Fontsdevelopers.cloudflare.comWindow: requestIdleCallback() method - Web APIs | MDNdeveloper.mozilla.orgTemplate directives referencedocs.astro.buildBot Fight Modedevelopers.cloudflare.com

脚注

  1. PageSpeed Insights。Google が提供する Web ページのパフォーマンス計測ツール。モバイルとデスクトップそれぞれに 0〜100 のスコアを付ける。 ↩本文へ戻る

  2. First Contentful Paint。ページを開いてから最初のコンテンツ(テキストや画像)が描画されるまでの時間。 ↩本文へ戻る ↩2本文へ戻る (2 箇所目)

  3. HTTP Archive。ブラウザがやりとりした全リクエストのタイミングやサイズを記録した JSON 形式のファイル。DevTools からエクスポートできる。 ↩本文へ戻る

  4. Largest Contentful Paint。ビューポート内で最大のコンテンツが描画されるまでの時間。Core Web Vitals の指標のひとつ。 ↩本文へ戻る

この記事を Markdown で取得