
XのiOSアプリが外部サイトをPrefetch(先読み)する仕様とその影響
2025年11月、XのiOSアプリが外部リンクを事前読み込みする仕様に変更。バズ発生時にサーバー負荷だけ増えてPVがカウントされない問題とその対策を解説します。
公開日2025.12.30
更新日2025.12.30
はじめに
2025年11月、Xアプリ(旧Twitter iOS版)が外部リンクを事前読み込み(Prefetch)する仕様に変更されました。
この仕様変更により、バズが発生した際に「サーバー負荷だけ増えてPVとしてカウントされない」という問題が発生するケースがあります。この記事では、Xの新仕様の詳細と、実際に発生する問題、そして対策について解説します。
目次
対象読者
- Xで投稿がバズることがあるサイト運営者
- サーバー負荷の原因を調査している開発者
- SSR(Server-Side Rendering)を使用しているWebサイト運営者
Xアプリの新しいPrefetch仕様とは
2025年11月の仕様変更
Xアプリ(iOS版)は2025年11月頃から、タイムライン上の外部リンクに対して プリフェッチ(Prefetch / 先読み) を実装しました。
従来の挙動:
- ユーザーがリンクをタップ → コンテンツを読み込み開始
新しい仕様:
- リンクが画面に表示された時点 → バックグラウンドでコンテンツの読み込みを開始
- ユーザーがタップする前から読み込みが発生
仕様変更の情報源
この仕様変更は、複数の技術メディアや開発者コミュニティで報告されています。
PPC Land(2025年11月8日)
"X iOS update disables JavaScript during link prefetch, generating artificial traffic"
2025年11月5日午後4時33分(現地時間)、XのiOSアプリに以下の設定変更が適用されました:
ios_in_app_article_webview_disable_javascript_during_prefetch: trueこの設定により、XアプリはリンクのPrefetch(事前読み込み)時にJavaScriptを無効化するようになりました。実際のクリック時には通常通りJavaScriptが有効化されます。
参考記事:
- PPC Land - X iOS update disables JavaScript during link prefetch
- 2025年11月5日の実装日時、MRC(Media Rating Council)基準への違反、特定ドメイン(Apple、Instagram等)の除外リストについて詳細に報告
- Hacker News - Tell HN: X is opening any tweet link in a webview
- 開発者コミュニティによる技術的な議論、eコマース事業者への影響、ウェブビューの問題点についての実体験が共有されている
Prefetchの技術的な特徴
Xアプリのプリフェッチは以下の技術的特性を持っています。
| 特徴 | 詳細 |
|---|---|
トリガー | リンクカードが画面に表示された時点 |
取得内容 | HTMLのみ |
JavaScript | 無効化された状態で取得 |
実装方式 | WKWebView(iOSのWebブラウザエンジン) |
OGPメタタグ | サーバー側で生成されたものは正常に取得可能 |
なぜこの仕様にしたのか
Xアプリがこの仕様を採用した理由として考えられるのは:
- UX向上:ユーザーがタップした瞬間に即座にページを表示できる
- 広告タグの誤発火防止:JSを無効化することで、プリフェッチ段階で広告が課金されることを防ぐ
- リンクプレビューの充実:OGPメタタグを事前に取得し、タイムラインでのプレビュー表示を向上
ユーザー体験の向上を目的とした仕様変更ですが、Webサイト側には予期しない影響が発生するケースがあります。
どんな時に問題になるのか:バズが発生した時
バズ時のPrefetchの影響
Xで投稿がバズると、以下のような流れでアクセスが発生します。
- 投稿が拡散される → タイムラインに表示される回数が急増
- 表示されただけでPrefetchが発火 → 数千〜数万のHTMLリクエストがサーバーに届く
- 実際にリンクをクリックする人 → その一部のみ
つまり、「表示された数」と「クリックされた数」の差が大きいほど、無駄なサーバー負荷が発生します。
具体例:投稿が1万回表示された場合
| 指標 | 従来の仕様 | 新しい仕様(Prefetch有効) |
|---|---|---|
タイムライン表示回数 | 10,000回 | 10,000回 |
サーバーへのHTMLリクエスト | 500回(クリック数) | 10,000回(表示数すべて) |
実際のクリック数 | 500回 | 500回 |
Google AnalyticsのPV | 500 | 500 |
サーバー負荷 | 通常 | 20倍 |
バズが発生すると、サーバー負荷だけが急増し、PV数やコンバージョンは比例して増えないという現象が起きます。
特に影響を受けやすいサイト
以下のようなサイトは、この問題の影響を受けやすい傾向にあります。
- SSR(Server-Side Rendering)を使用しているサイト:リクエストごとにサーバーでHTMLを生成
- DBアクセスが必要なページ:記事の本文や関連記事をDBから取得している
- 計算処理が重いページ:ランキング計算、レコメンデーション生成など
- バズりやすいコンテンツを扱うサイト:ニュースサイト、バイラルメディアなど
SSRサイトで発生する具体的な問題
Next.jsでSSRを使っている場合の例
Next.jsのSSR(Server-Side Rendering)を使用している場合、以下のような問題が発生します。
// app/posts/[id]/page.tsx
export default async function PostPage({ params }: { params: { id: string } }) {
// Prefetch時にもこの処理が実行される
const post = await fetchPostFromDB(params.id);
const relatedPosts = await fetchRelatedPosts(params.id);
return (
<div>
<h1>{post.title}</h1>
<PostContent content={post.content} />
{/* Google Analyticsなどのタグ(JSが無効なため発火しない) */}
<Analytics />
</div>
);
}この構成では、以下の問題が発生します。
| 処理 | Prefetch時の動作 | 実際のクリック時の動作 | 問題点 |
|---|---|---|---|
サーバー側のHTML生成 | ✅ 実行される | ✅ 実行される | Prefetchでもサーバー負荷が発生 |
DBアクセス | ✅ 実行される | ✅ 実行される | 無駄なDB負荷 |
計算処理 | ✅ 実行される | ✅ 実行される | 無駄なCPU使用 |
Google Analytics | ❌ 発火しない(JS無効) | ✅ 発火する | PVとして計測されない |
その他の計測タグ | ❌ 発火しない | ✅ 発火する | アクセス解析に反映されない |
結果として、サーバー負荷だけ消費してPVとしてカウントされないという非効率な状態になります。
CSR(Client-Side Rendering)の場合は影響を受けない
一方、CSR(クライアントサイドレンダリング)を使っているサイトは、この問題の影響を受けにくい傾向にあります。
// クライアントサイドでデータ取得する場合
'use client';
export default function PostPage() {
const [post, setPost] = useState(null);
useEffect(() => {
// JSが無効なPrefetch時は実行されない
fetchPost().then(setPost);
}, []);
return <div>{post?.content}</div>;
}XのPrefetchはJSを無効化しているため、useEffect内の処理は実行されません。サーバー負荷は発生しませんが、OGPメタタグが生成されないというデメリットもあります。
対策:Next.jsの場合はLazy ISRを導入
筆者の事例:100万件近いデータでの対策
筆者が運営しているサイトでは、以下の状況でした。
- ページ数:約100万件(ユーザー投稿ベース)
- 構成:Next.js App RouterのSSR
- 課題:Xでバズった際にサーバー負荷が急増
この状況で、「事前にすべてのページをビルド(Static Site Generation)」することは現実的ではありませんでした。
そこで採用したのが Lazy ISR(Incremental Static Regeneration) という手法です。
Lazy ISRとは
Lazy ISRは、Next.jsのISR機能を活用した戦略で、以下の特徴を持ちます。
- ビルド時には静的ページを生成しない(
generateStaticParamsで空配列を返す) - 初回アクセス時にページを静的生成してキャッシュ
- 一度生成されたページはCDNから配信される
この戦略により、XのPrefetchが 「キャッシュ生成のトリガー」 として機能します。
100万件のページで事前ビルドしない理由
従来のSSG(Static Site Generation)では、ビルド時に全ページを生成します。
// 従来のSSG(100万件では非現実的)
export async function generateStaticParams() {
// 全ページのIDを取得してビルド時に生成
const allPostIds = await fetchAllPostIds() // 100万件!
return allPostIds.map((id) => ({ id }))
}100万件のページを事前ビルドすると:
- ビルド時間が数時間〜数十時間かかる
- ストレージ容量が膨大になる
- デプロイのたびに全再ビルドが必要
そのため、 アクセスがあったページだけを静的化する「Lazy ISR」 を採用しました。
実装方法(Next.js App Router)
筆者が実際に設定したコードは以下の通りです。
// app/posts/[id]/page.tsx
// 事前ビルドは行わない(空配列を返す)
export async function generateStaticParams() {
return []; // 100万件を事前ビルドしない
}
// ISRを有効化(1時間キャッシュ)
export const revalidate = 3600;
export default async function PostPage({ params }: { params: { id: string } }) {
// 初回アクセス時のみサーバー処理が実行される
const post = await fetchPostFromDB(params.id);
const relatedPosts = await fetchRelatedPosts(params.id);
return (
<div>
<h1>{post.title}</h1>
<PostContent content={post.content} />
<Analytics />
</div>
);
}設定の2つのポイント
1. generateStaticParamsで空配列を返す
export async function generateStaticParams() {
return [] // 事前ビルドなし
}事前ビルドをスキップし、アクセスがあったページだけを静的化します。
2. revalidateでキャッシュ期間を設定
export const revalidate = 3600 // 1時間(秒単位)revalidateを設定することでISRが有効になり、指定した期間キャッシュが保持されます。
コンテンツの更新頻度に応じて設定を調整してください:
3600(1時間):頻繁に更新されるコンテンツ86400(24時間):あまり更新されないコンテンツ
XのPrefetchに対するLazy ISRの効果
Lazy ISRを導入すると、XのPrefetchは以下のように作用します。
| アクセス段階 | 処理内容 | サーバー負荷 | レスポンス元 |
|---|---|---|---|
1. XのPrefetch(初回) | サーバーでHTML生成 → CDNにキャッシュ保存 | ✅ あり | サーバー |
2. ユーザーの実際のクリック | キャッシュされたHTMLを配信 | ❌ なし | CDN |
3. 別のユーザーのアクセス | キャッシュから配信 | ❌ なし | CDN |
4. revalidate期間経過後 | バックグラウンドで再生成 | ✅ あり(1回のみ) | CDN(古いキャッシュを返す) |
重要なポイント:
- XのPrefetchが「初回アクセス = キャッシュ生成のトリガー」として機能
- 実際のユーザーアクセスはすでにキャッシュ済みのため、サーバー負荷ゼロで高速配信
- バズ時も、1回目のPrefetchでキャッシュが作られれば、それ以降はCDNから配信される
導入前後の比較
筆者のサイトでは、Lazy ISR導入前後で以下のような変化がありました。
| 指標 | Lazy ISR導入前(SSR) | Lazy ISR導入後 |
|---|---|---|
バズ時のサーバー負荷 | タイムライン表示数分のHTMLレンダリング | 初回のみ |
CDNヒット率 | - | 90%以上 |
サーバーコスト | 高い | 大幅に削減 |
ページ表示速度 | 普通 | 高速(CDN配信) |
その他の対策・注意点
CSRに切り替える選択肢もある
もしOGPメタタグが不要な場合は、CSR(Client-Side Rendering)に切り替えることでPrefetchの影響を完全に回避できます。
'use client';
export default function PostPage() {
const [post, setPost] = useState(null);
useEffect(() => {
// JSが無効なPrefetch時は実行されない
fetchPost().then(setPost);
}, []);
return <div>{post?.content}</div>;
}ただし、X上でのリンクプレビュー(OGP画像・タイトル)が表示されなくなるデメリットがあります。
ユーザー固有コンテンツの扱い
キャッシュと相性が悪いユーザーごとのコンテンツ(ログイン状態など)は、クライアントサイドで取得する必要があります。
export default async function Page() {
// 全ユーザー共通部分(キャッシュ可能)
const post = await fetchPost();
return (
<div>
<PostContent post={post} />
{/* ユーザー固有部分はクライアントで取得 */}
<UserSpecificContent />
</div>
);
}まとめ
XのiOSアプリPrefetch仕様のポイント
- 2025年11月から実装:リンクが画面に表示された時点でHTMLを事前読み込み
- JavaScript無効化:計測タグが発火しないため、PVとしてカウントされない
- バズ時に影響大:タイムライン表示数分のHTMLリクエストが発生
問題が発生するサイト
- SSR(Server-Side Rendering)を使用しているサイト
- DBアクセスや計算処理が必要なページ
- バズることがあるコンテンツを扱うサイト
Next.jsでの対策事例
筆者の事例(約100万ページのサイト)では、Lazy ISRを導入することで:
- ✅ 事前ビルド不要で大規模サイトにも適用可能
- ✅ XのPrefetchをキャッシュ生成トリガーとして活用
- ✅ バズ時も初回のみサーバー負荷、以降はCDN配信
- ✅ サーバーコストを大幅に削減
XアプリのPrefetch仕様は、適切に対応すれば逆にパフォーマンス最適化の機会になります。自サイトの構成に応じて、Lazy ISRやCSRへの切り替えを検討してみてください。
参考資料
お問い合わせはこちらから
ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。
