はじめに
スマホブラウザの「PC版サイトを表示」が裏で何をやっているのか気になったので、調査をしてみました。
長いので、結論だけ知りたい場合は こちら へ
仮説
まずは仮説を立てて、それを検証して、裏付けるといった感じで進めます。
仮説として、思いつく候補は2つです。
1. User Agent(UA)の偽装
ブラウザはリクエストヘッダーに UA を含めて送信します。
サーバー側で UA を見てコンテンツを出し分けているサイトでは、UA をデスクトップ用に変えれば PC 向けのコンテンツが返ってきます。
これは、モバイル用のサイトは m.example.com にして、PC 版サイトは example.com にしているような、昔ながらのサイトでの動作から予想できます。
2. Viewport 幅の変更
レスポンシブデザインのサイトは CSS メディアクエリで画面幅を見てレイアウトを切り替えています。
width=device-width の viewport 指定を無視して幅を広くすれば、PC 向けのレイアウトが表示されます。
実際、「PC版サイト表示」を ON にするとレスポンシブなサイトが PC 向けレイアウトに変わります。
これは、viewport 幅が変わっているからと推測できます。
検証
1. UA の変更を確認する
Chrome DevTools の Network conditions パネルを使うと、viewport はそのままで UA だけを変更することができます。
- DevTools → ⋮ → More tools → Network conditions
- User agent の "Use browser default" のチェックを外す
- プルダウンから "Chrome — Android Mobile" などを選択
device emulation モード(ツールバーのモバイルアイコン)は UA と viewport を両方変えてしまい、切り分けにならないため、ここでは利用しません。
この状態で、https://www.yahoo.co.jp/ にアクセスすると、m.yahoo.co.jp にリダイレクトされました。UA を元に戻すと yahoo.co.jp に戻ります。
UA を変えるだけでリダイレクトが起きたので、Yahoo はサーバーサイドで UA を見て URL を振り分けていることがわかります。
2. viewport の変更を確認する
UA は PC のままで、viewport だけを狭めて https://www.yahoo.co.jp/ にアクセスしてみます。
これでは、リダイレクトは起きませんでした。
viewport 幅はサーバーには届かないので、これは当たり前かもしれません。
一方、Qiita 等のレスポンシブ設計のサイトは、viewport 幅を狭めるだけでモバイル向けレイアウトに切り替わります。
CSS メディアクエリが viewport 幅に反応するからです。
検証結果
検証内容をまとめると、2つの変更はそれぞれ別の役割を持っています。
| 変更 | サーバーサイドへの影響 | レンダリングへの影響 |
|---|---|---|
| UA 変更 | UA 検出しているサイトが別コンテンツを返す | なし |
| viewport 変更 | なし | レスポンシブ CSS が反応してレイアウトが変わる |
旧来型のサイト(m.yahoo.co.jp への振り分けなど)には UA 変更が、レスポンシブデザインのサイトには viewport 変更が効きます。
したがって、この両方のサイトの状態に対応できる「PC版サイト表示」は、UA 変更と viewport 変更の両方を行っていると言えます。
更なる検証
Chromium のコードを実際に見ることで、裏付けを行います。
UA 変更の実装
Chromium の UA 生成コードを見ると、Mobile トークンはデフォルトではなく、条件付きで追加される仕組みになっています。
std::string GetUserAgentInternal() {
...
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
if (base::CommandLine::ForCurrentProcess()->HasSwitch(kUseMobileUserAgent)) {
product += " Mobile";
}
#endif
...
}
Android の Chrome はプロセス起動時に kUseMobileUserAgent スイッチを設定するため、通常は Mobile 付きの UA になります。
「PC版サイト表示」はこのスイッチが立っていない状態の UA を代わりに使う操作です。
「PC版サイト表示」をオンにすると、NavigationController.setUseDesktopUserAgent() が呼ばれ、C++ 側の SetUseDesktopUserAgentInternal() に処理が渡ります。
void NavigationControllerAndroid::SetUseDesktopUserAgentInternal(
bool enabled,
bool reload_on_state_change,
bool skip_on_initial_navigation) {
...
// Set the flag in the NavigationEntry.
entry->SetIsOverridingUserAgent(enabled);
navigation_controller_->delegate()->UpdateOverridingUserAgent();
...
}
IsOverridingUserAgent() フラグが現在の NavigationEntry に保存され、ページがリロードされます。
リロード時には kUseMobileUserAgent スイッチなしで生成した UA が使われ、Mobile トークンが消えます。
Android の Chrome での UA の変化は以下のようになります。
// 通常時(kUseMobileUserAgent あり)
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/X Mobile Safari/537.36
// PC版サイト表示時(kUseMobileUserAgent なし相当)
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/X Safari/537.36
Chrome 107 以降の UA Reduction により platform 文字列は Linux; Android 10; K に固定されているため、PC版表示に切り替えても platform 部分は変わりません。
変わるのは Mobile トークンの有無だけです。
また、Sec-CH-UA-Mobile ヘッダーも同じ kUseMobileUserAgent スイッチで制御されています。
bool GetMobileBitForUAMetadata() {
// The mobile bit for UA-CH is true if the platform is iOS, or if it's
// Android and not a desktop form factor, AND the kUseMobileUserAgent switch
// is present.
#if BUILDFLAG(IS_ANDROID)
if (base::android::device_info::is_desktop() ||
base::android::device_info::is_xr()) {
return false;
}
#endif
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
return base::CommandLine::ForCurrentProcess()->HasSwitch(kUseMobileUserAgent);
#else
return false;
#endif
}
UA 文字列に Mobile を付加するかどうかと、Sec-CH-UA-Mobile を ?1 にするかどうかが、同じスイッチで一元管理されています。
PC 版表示に切り替えると、UA 文字列と Sec-CH-UA-Mobile の両方がまとめて変わります。
viewport 変更の実装
viewport_meta_enabled フラグも UA と同じ構造で、Android/iOS ではモバイル向けにデフォルトで viewport_meta_enabled = true が設定されています。
bool viewport_meta_enabled = BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS);
UA override がアクティブになると、web_contents_impl.cc でこのフラグが false にリセットされます。
if (GetController().GetVisibleEntry() &&
GetController().GetVisibleEntry()->GetIsOverridingUserAgent()) {
#if BUILDFLAG(IS_ANDROID)
// Only ignore viewport meta tag when Request Desktop Site is used, but not
// in other situations where embedder changes to arbitrary mobile UA string.
is_request_android_desktop_site =
renderer_preferences_.user_agent_override.ua_metadata_override &&
!renderer_preferences_.user_agent_override.ua_metadata_override->mobile;
prefs.viewport_meta_enabled = !is_request_android_desktop_site;
#else
prefs.viewport_meta_enabled = false;
#endif
}
また、レンダリングエンジン側では GetViewportDescription() でこのフラグを参照し、<meta name="viewport"> のパース結果を適用するかどうかを制御しています。
ViewportDescription ViewportData::GetViewportDescription() const {
ViewportDescription applied_viewport_description = viewport_description_;
bool viewport_meta_enabled =
document_->GetSettings() &&
document_->GetSettings()->GetViewportMetaEnabled();
if (legacy_viewport_description_.type !=
ViewportDescription::kUserAgentStyleSheet &&
viewport_meta_enabled)
applied_viewport_description = legacy_viewport_description_;
if (ShouldOverrideLegacyDescription(viewport_description_.type))
applied_viewport_description = viewport_description_;
...
}
legacy_viewport_description_ は、<meta name="viewport"> をパースした結果です。viewport_meta_enabled が false のとき、この値は applied_viewport_description に代入されず、viewport_description_(UA のデフォルト)がそのまま返ります。
この UA のデフォルト viewport 幅は、viewport_style_resolver.cc の ViewportStyle::kMobile ケースで定義されています。
// We only want to use the device scale portion of the zoom factor, because
// the page layout size should remain fixed relative to page zoom in order
// to reflow into it.
case mojom::blink::ViewportStyle::kMobile: {
description.min_width = ViewportLength::Fixed(980 * DeviceScaleZoom());
return description;
}
したがって、<meta name="viewport"> のパース結果が適用されない場合は、この 980px がデフォルト viewport 幅になります。
まとめ
「PC版サイト表示」は、Chrome がモバイル向けに施した2つの設定をそれぞれ解除する操作です。
-
kUseMobileUserAgentスイッチで追加されていた、Mobileトークンを UA から除く- 同じスイッチで制御される
Sec-CH-UA-Mobileも?1から?0に変わる
- 同じスイッチで制御される
-
viewport_meta_enabledをfalseにすることで、<meta name="viewport">の代わりにデフォルトの 980px viewport を使わせる
Chromium の内部では NavigationController.setUseDesktopUserAgent() がトリガーとなり、この2つが同時に処理されます。