iOS safari 9.2 で window.innerWidth と viewport の値がズレる

More than 1 year has passed since last update.

レスポンシブデザインのページで、 window.innerWidth の値によって条件分岐するような js を書いていたとき、 iOS だけ条件分岐がうまくいってないことが判明。
ちなみに、 Chrome DevTools で iOS 系のデバイスモードにしているときと Android は 問題なく、iOS 実機及び iOS Simulator でのみ発生していました。

画面幅に関連する値を取得してみる

まずは計測ということで、以下のような HTML を用意して画面幅に関する3つの値、 window.innerWidthscreen.widthdocument.documentElement.clientWidth を取得してみました。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>viewport test</title>
</head>
<body>
  <h1>viewport test</h1>
  <p id="text"></p>
  <script>
    window.addEventListener('load', function(){
      function printWidth(){
        document.getElementById('text').innerHTML = 
        'window.innerWidth: '+window.innerWidth+
        '<br>screen.width: '+screen.width+
        '<br>documentElement.clientWidth: '+document.documentElement.clientWidth;
      }
      printWidth();
    });
  </script>
</body>
</html>

iOS simulator の結果(iOS9.2 iPhone6)

simulator_1.png

window.innerWidth が 980 と、document.documentElement.clientWidth の 375 とは大幅に違う値を示しています。
ここで、 safari のコンソールから window.innerWidth を取得してみると、

simulator_1_a.png

?!?!
なぜ?!?!?!

Chrome DevTools の結果(iPhone6)

chrome_1.png

こっちは想定どおりの値です。
コンソールから window.innerWidth を取得しても、

chrome_1_a.png

納得の結果です。

そもそも window.innerWidth とか document.documentElement.clientWidth って何の値なんだ

window.innerWidthMDN によると、

垂直スクロールバー(表示されている場合)を含む、ブラウザウィンドウの ビューポート (viewport) の幅を返します。

とのこと。

一方、document.documentElement は 同じく MDNによると、

document のルート要素 (HTML 文書の場合は <html> 要素) を返します。

であり、それの clientWidth ということは、同じく MDN によると、

clientWidth は element 内のピクセル単位の幅を返します。 paddingを含み、垂直スクロールバー(存在するならば)、border、marginを含みません。

なので、 document.documentElement.clientWidth<html> 要素の padding を含むが、垂直スクロールバー、border、 margin 含まない幅。

おおまかにいうと、垂直スクロールバーを含むか含まないかが違いそうです。
そのため、ブラウザによっては必ずしも全く同じではないことがわかります。
あと、document.documentElement.clientWidth でもいいか、と思ってたけど、厳密に viewport で判定したいときは window.innerWidth を使うべきだということもわかった。

ただし、今回スクロールバーは出てない文章量だし、980 と 375 っていう倍以上の差は一体何からうまれるのか…

色々試してみたら、一致するパターンがあった

色々と書き換えてみたところ、以下のように window.innerWidth の前に document.documentElement.clientWidth を取得してみたら、期待通りの値を取得できた。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>viewport test</title>
</head>
<body>
  <h1>viewport test</h1>
  <p id="text"></p>
  <script>
    window.addEventListener('load', function(){
      function printWidth(){
        document.getElementById('text').innerHTML = 
        'documentElement.clientWidth: '+document.documentElement.clientWidth+
        '<br>screen.width: '+screen.width+
        '<br>window.innerWidth: '+window.innerWidth;
      }
      printWidth();
    });
  </script>
</body>
</html>

iOS simulator の結果(iOS9.2 iPhone6)

simulator_2.png

Chrome DevTools の結果(iPhone6)

chrome_2.png

まとめ

iOS で window.innerWidth の取得値が期待通りにいかないときは、先に document.documentElement.clientWidth を取得するとうまくいくかもよ。

なぜなのか

わかりません。というかこの現象は必ず起こるものなのか。
どなたかご存知の方がいらっしゃいましたらコメントなどでお知らせ頂けますと助かります。