viewportタグがChromeデスクトップ版では効かない挙動を追ってみた

## 初めに

HTMLのviewport、レスポンシブ対応に呪文のように毎回唱えるけど、結局何をやっているのかが分からないがちです。
そこで色々と設定値を変えながら試していたのですが、予期せぬ挙動に頭を悩まされました。
「viewportでwidthを500pxと指定しているのに、window.innerWidthの値が1000を超えているぞ！？」みたいなことが。。
色々試していると、どうやらモバイルエミュレータなしデスクトップ版Chromeだとviewportが反映されないみたいです。
この挙動について、ネット上の記事を漁りましたがモバイル版でしか機能しない趣旨のものが見つからず、仕様と実装を軽く追うことにしました。

## 環境
- Chromeバージョン: 107.0.5304.87（Official Build） （arm64）

## そもそもviewportとは

viewportとは現在表示されている画面領域のことを言います。
一度仮想的な画面を指定したviewportのサイズを元にブラウザ内部で作成し、それを画面の解像度に合わせるように拡大縮小して描画します。

```html
<meta content='viewport' width='500px' />
```

例えば上のコードで解像度320pxの画面に表示する場合は、一度横幅500pxで内部的に描画した後、それを320pxに縮小します。フォントを16pxで指定していた場合、この仮想的なウィンドウの縮小により文字サイズも同様に小さくなります。

## viewportが効いているのか判定方法

機能しているとwindow.innerWidthの値がviewportのwidth値と等しくなるので、その値で判定します。
window.innerWidthの説明は[MDN](https://developer.mozilla.org/ja/docs/Web/API/Window/innerWidth)から引用すると、

> innerWidth の値はウィンドウのレイアウトビューポートの幅から取られます。

viewportにはlayout viewportとvisual viewportの２種類ありますが、ここでは解説しません。
[MDNのビューポートの概念](https://developer.mozilla.org/ja/docs/Web/CSS/Viewport_concepts)を確認してください。

## 実験

では実際にデスクトップ版Chromeでviewportが効いているか試してみましょう。
今回実験に使うコードは以下です。
```html
<html>
    <head>
        <meta name="viewport" content="width=500">
    </head>
    <body>
        <p>window.innerWidth = <span id="innerWidth"></span></p>
        <script>
            window.addEventListener('load', () => {
                document.getElementById('innerWidth').innerHTML = window.innerWidth
            })
        </script>
    </body>
</html>
```

デスクトップ版Chrome, デスクトップ版Chrome (モバイルエミュレータ), iPhone版Chromeで試しました。
結果はデスクトップ版Chromeのみ500pxにならず、他は意図した挙動になりました。

#### デスクトップ版Chrome
![](https://i.imgur.com/B9AArYn.png)

#### デスクトップ版Chrome (モバイルエミュレータ)
![](https://i.imgur.com/3iKGf0R.png)

#### iPhone版Chrome
スクリーンショットはないですが、500pxでした。


## この挙動はどうして？

デスクトップ版だけviewportのmetaタグが効かないのはどうしてなんでしょうか？
調べてもそれらしき解説を発見できなかったので、viewportを定義している規格書を見に行きました。
初めはHTMLのタグの一つなのでHTMLの規格書を探したのですが、どうやらCSSの
[CSS Device Adaptation Module Level 1](https://www.w3.org/TR/css-device-adapt-1/)で定義されているみたいです。
HTMLのタグを内部的にCSSの`@viewport`に変換しているらしいです。

> 9.4. Translation into @viewport descriptors 
The Viewport `<META>` element is placed in the cascade as if it was a `<STYLE>` element, in the exact same place in the dom, that only contains a single @viewport rule.

訳 (DeepL先生)
> 9.4. viewport 記述子への変換 ビューポートの `<META>` 要素は、あたかも `<STYLE>` 要素のようにカスケードに配置され、dom の全く同じ場所にあり、単一の @viewport ルールだけを含んでいます。

さて、デスクトップブラウザでviewportを適応するのか。
それらしきことが記載された部分は以下でした。

> UA vendors implementing this specification are strongly encouraged to do so both for their mobile and desktop browsers. The @viewport mechanism is designed to be usable and useful on all browsers, not only mobile ones. However, if support is only available on mobile browsers for a significant time, there is a risk that authors would write @viewport rules that work on mobile but do the wrong if applied by a desktop browser. This would make it difficult to later add support for @viewport in desktop browsers.

訳 (DeepL先生)
> この仕様を実装するUAベンダーは、モバイルブラウザとデスクトップブラウザの両方について実装することが強く推奨される。viewport機構は、モバイルブラウザだけでなく、すべてのブラウザで使用可能で有用であるように設計されています。しかし、もしモバイルブラウザでのみかなりの期間サポートされると、作者がモバイルで動作する@viewportルールを書いても、デスクトップブラウザで適用すると間違った動作をする恐れがあります。そうなると、後でデスクトップ用ブラウザで@viewportのサポートを追加することが難しくなります。

注意書きでこのように記載されていることは、モバイルのみ実装しているベンダーが多いのでしょうか？
どうやら、viewportはデスクトップ、モバイル両方に担当する必要は規格的には無さそうです。(強く勧められてはいますが)

この時点で、恐らくChromeも対応していないんだなーと予想できます。規格書でも述べられているよう、後方互換性が問題で対応できないのでしょうかね？

もう一歩踏み込んでChromeのベースであるChroniumを読み、Chromeでは未対応であることを確認してみようと思います。

## リーディングするOSS

Chroniumのソースコードは巨大なジャングルでどこを読めばいいのかさっぱりでしたが、ChroniumレンダラーエンジンのBlinkにそれらしき処理がありました。

HTML, CSS, JavaScriptのパース、画面のレンダリング部分を担当しています。

リーディングは[Chronium Code Search](https://source.chromium.org/chromium/chromium/src)の使い勝手が非常に良かったです。

結論から言うと、[third_party/blink/common/web_preferences/web_preferences.cc](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/common/web_preferences/web_preferences.cc;l=116)にそれらしき処理が記載されていました。

```cpp
#if BUILDFLAG(IS_ANDROID)
      viewport_meta_enabled(true),
      auto_zoom_focused_editable_to_legible_scale(true),
      shrinks_viewport_contents_to_fit(true),
      viewport_style(mojom::ViewportStyle::kMobile),
      always_show_context_menu_on_touch(false),
      smooth_scroll_for_find_enabled(true),
      main_frame_resizes_are_orientation_changes(true),
#else
      viewport_meta_enabled(false),
      auto_zoom_focused_editable_to_legible_scale(false),
      shrinks_viewport_contents_to_fit(false),
      viewport_style(mojom::ViewportStyle::kDefault),
      always_show_context_menu_on_touch(true),
      smooth_scroll_for_find_enabled(false),
      main_frame_resizes_are_orientation_changes(false),
#endif
```

viewport_meta_enabledがviewportのmetaタグを使用可能かを設定しているっぽいです。
ANDROIDならtrue, それ以外ならfalseです。iPhoneはこれらとは別にプロジェクトが進行しているらしいので、このコード内では考慮されていません。

ただ、エミュレータ起動時の処理は別にあるはずです。
そちらも追ってみます。

こちらの処理は、[third_party/blink/renderer/core/inspector/dev_tools_emulator.cc](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/dev_tools_emulator.cc;l=338?q=emulator&ss=chromium%2Fchromium%2Fsrc:third_party%2Fblink%2F)に記載されていました。
名前からエミュレータ起動時の設定ですね。
このメソッドの中でSetViewportMetaEnabledが呼ばれていることがわかります。
```cpp
void DevToolsEmulator::EnableMobileEmulation() {
  ...
  web_view_->GetPage()->GetSettings().SetViewportEnabled(true);
  web_view_->GetPage()->GetSettings().SetViewportMetaEnabled(true);
  web_view_->GetPage()->GetSettings().SetShrinksViewportContentToFit(true);
  web_view_->GetPage()->GetSettings().SetTextAutosizingEnabled(true);
  web_view_->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled(
      true);
  web_view_->GetPage()->GetSettings().SetPluginsEnabled(false);
  web_view_->GetPage()->GetSettings().SetMainFrameResizesAreOrientationChanges(
      true);
  web_view_->SetZoomFactorOverride(1);
  web_view_->GetPage()->SetDefaultPageScaleLimits(0.25f, 5);
  ...
}
```

## 最後に
今回はデスクトップ版Chromeのviewportの挙動を追ってみました。
ソースコードから処理を追いましたが、設定を変えてChroniumをビルドし直す気力は無かったので信頼度は低いです。
もう少し余裕がある時に試してみようと思います。(試して頂けたら嬉しいです。)
最後まで読んでいただきありがとうございました。