Edited at

Chromeのloading属性は、指定しなくてもデフォルトで"lazy"の挙動となりうる ~loading属性の落とし穴から学ぶ、機能拡張と初期値・デフォルト挙動の話~


【事前知識】loading属性について

Chromeのimgiframe要素には、loading属性というものがある。

こいつをHTMLでloading="lazy"のように指定すると、遅延読み込みする(画像等がスクロール等で画面内に入ってきた時に初めて読み込み始める)。

こうすることで、時間がかかる無駄な読み込みが抑えられ、ページ表示速度が速くなったり、通信量を減らせたりする。

値には次の3種類があり、"lazy"が遅延読み込みオン、"eager"は遅延読み込みオフ(遅延せず愚直にすぐ読み込み)、"auto"はlazyかeagerかをChromeがネットワーク速度などを考えて自動で決める。

これはChrome 75の時に登場した新機能で、Chrome独自の試験的なものらしい。これまでは同じことをするのにJavaScriptを使わねばならなかったのが手軽に書けるようになったとあって、人気のようだ。


【事前知識2】loading属性とonloadの落とし穴

普段はプラスの側面しか見えず無害に見えるloading属性(遅延読み込み)だが、こいつには盛大な落とし穴がある。

それは、ページ全体に対するonloadイベントがあてにならなくなる点だ。

onloadイベントには、要素が完全に読み込まれたら実行する処理を書くことができる。img要素に指定すればその画像が、body要素やwindow等ページ全体に指定すればページ内の本文・画像・iframe・動画等全ての要素が読み込み完了した時に処理が走る。

例えば、複数ある画像の縦横比が不定のためimg要素の高さをheight:autoとせざるを得ず、高さがあらかじめ分からないが、どうしても画像を含めたページ全体の高さをJavaScriptで取得する必要がある時。

こんな時、ページ全体へのonloadイベントが便利だ。読み込み完了時には全ての画像が出そろっているため、高さが判明しているからだ。

しかし、loading="lazy"はこの便利なカラクリを破壊する。

loading="lazy"によって遅延読み込みが指定された画像は、ページ下部にある場合、スクロールするなどして画面内に顔を出さない限り読み込まれない。しかしこの時、ページ上部の画像だけ読み込んだところで先にページ全体のonload処理が走ってしまうまだ下部の画像読み込みがloading="lazy"によって保留されてるにも拘らず、あたかもページ内の画像全てが読み込み終わったかのようにonloadが発動してしまうのだ。ようは、onloadのフライングである。

loadingは読み込みを遅延させるのだから、これがonloadの挙動に影響することは考えれば当然なのだが、この挙動はあまりにもトラップがすぎるというか、直感に反する。酷い落とし穴だ。

だが、こんな仕様になったのもしょうがないのかもしれない。

というのも、もしonloadが従来通り正真正銘の完全読み込み後にしか発動しない挙動を貫いていたら、ページ内を隅から隅までスクロールしきって、loading="lazy"な要素を全て読み込みきらなければonloadが走ってくれないことになる。それはそれで困る。だから、このようにフライングさせるようにしたのだろう。

よって、ページ内の全画像等の読み込み完了時に走らせたいonload処理がある場合は、loading="lazy"を使ってはいけない


ワシ「そっか!何でもかんでもloading="lazy"を付ければいいって訳じゃないんやな!」

ワシ「じゃあ、読み込み完了時に走らせたい処理があるこのページでは、loading="lazy"を付けないで、従来通りimgを書けばいいんやな!

しかし・・・そこに更なる落とし穴が潜んでいたのである・・・


【本題】loading属性の初期値は"auto"である

これまで私は、loadingという属性を明示的に書かない限り、今回の新機能は発動しないと思い込んでいた。

画像含めページ全体が読み込み終わってからonloadするページは、これまでずっとうまく動いてくれていたし、実際、今も目の前のPCのテストで、通常の読み込み・F12のレスポンシブモードによる読み込み共に、onloadはうまく動いている。

これからも、loading="lazy"さえ書かなければ変わらない、そう信じていた。なにせ、試験的な拡張機能なのだから。

しかし、ふと思い立って手元のAndroid実機のChromeでそのページを開いてみると・・・

ファッ!?!?!?!?!?onloadで取得する数値がズレている!?!?なんかデカい画像の高さの分だけズレてる気がするな。しかも、他のブラウザではズレないな。これはもしや、loading="lazy"の仕業か・・・?? でも、loading="lazy"など一言も書いていないから遅延読み込みはさせていないはずだし、実際PCではうまく動くぞ!?そもそもloadingなんていう属性には微塵も触れていないぞ!?なぜ実機だけ遅延読み込みになるんだ!?これはどういうことだ!!!

つまりそのページでは、

PCではF12のレスポンシブモードだろうが通常の読み込みloading="eager"相当)、Android実機では遅延読み込みloading="lazy"相当)が発生していたのである。

loading="lazy"を書いてないにも拘らず、Android実機から見るとあたかもloading="lazy"を指定したかのような挙動を見せたのだ。


ここで、loadingについて解説する数々のページをググり、真相を確かめに行った。こいつはChrome独自拡張のため、MDNには見当たらない。個々人のブログに頼ることとなった。また、自分自身でもテストコードをいくつか書き、挙動を検証していった。

そこで判明したのが、loading属性の初期値は"auto"であるという事実、

そして更に、loading属性は、書かなくても暗黙にその初期値が指定されたものとみなされる、つまりloading="auto"が書かれたものとみなされるという事実だった。

ここで、私は

「ちょっとちょっとオイオイオイChromeさん!?!?いくら何でもそれは勝手すぎでは????」

と思ったのだった。


まず、【事前知識】loading属性についてでも触れた、loading="auto"の挙動を復習する。


autolazyeagerかをChromeがネットワーク速度などを考えて自動で決める。


「lazyかeagerかをChromeが自動で決める。」つまり、loading="auto"の場合は、ネットワーク環境次第でloading="lazy"の挙動になる可能性がある、ということだ。

そして、先程言った通り、

Chromeでは(loading対応要素であるimgiframeに)loading="auto"書かなくても、書いたのと同じと見なされる。

つまり、まとめると・・・

Chromeでは、loading属性を全く書かずに従来の方法でimgiframeを書いたとしても、暗黙にデフォルトでloading="auto"が指定された扱いになり、ネットワーク環境次第で意図せずloading="lazy"の挙動を起こしてしまい、ページ全体のonloadがあてにならなくなってしまうのである・・・!!!!!!!!

驚愕の事実だ。非常にまずい事態。過去に動かしてきたコードが今でも動くかどうか、再検証を強いられたのだ。まさか、Chromeでこんなハメに陥るとは。IEかよ…


これで、問題のページの挙動に説明がつく。

テストPCでは通常の読み込み、Android実機では遅延読み込みが発生していたのは、

テストPCの繋がっていた有線のネットワークは速度が速かったためにChromeがeager読み込みできると判断した一方、

Android実機の繋げていた携帯回線はネットワーク速度が遅かったために、Chromeが勝手にlazy読み込みをし、その結果onloadバグを起こしていた、ということだ。

そして、私はそれまで、loading属性の初期値は"eager"だと思い込んでいたのだ。

Chromeの独自拡張であるloading属性を書かない時の挙動は、拡張前及び他ブラウザと同じ挙動、つまりloading="eager"の挙動であってしかるべき、と思っていたのだ。

でも、実際のChromeはそうではなかったのだ。autoだったのだ・・・

img要素にloadingって書かなくても、loading="auto"と解釈されてしまう。過去の既存のHTML文書でもそうだ。

これって、重大な互換性破壊では・・・?(←後述する。)


【解決】対処法

件のonloadバグの発生メカニズムは上記の通りなので、対処法は当然、以下のようになる:

Chromeでは、ページ全体へのonloadイベントがあり、かつそのonloadがページ内全てのimgiframeの読み込み完了時を意図したものである場合、ページ内全てのimgiframeへのloading="eager"(=lazyな読み込みを無効化)の指定が必須となる。さもなくば、Chromeのonloadはあてにならない。

だが、今更全てにloading="eager"を付けてまわるなんてとんでもない労力だ。そんな時はJavaScriptで動的にloading="eager"を付けてもよい。つまり、document.getElementsByTagName("img")とかで要素取ってきてそれぞれのelement.loading="eager"とする。手元のChrome 77でテストしたが、それでもうまくいった。

IE対応やiOS対応というのはよくあるが、

シェアが多く、気の狂った馬鹿げたバグの少ない信頼の置けるChromeで対応が必須になるというのは、なんとも気持ち悪い。


【結論】初期値やデフォルトの挙動を把握しよう

教訓。

新しい機能や項目が登場したら、

何もしなかった場合の初期値・デフォルトの挙動を把握しよう。

その機能に触れなった場合、その機能が無効と見なされるのか、有効と見なされるのか。

その新機能は、初めから暗黙でオンなのか、それとも明示的に設定しないとオンにならないオプション的存在なのか。これがどちらなのかをはっきりさせとく必要があるのだ。暗黙というのは恐ろしい。

そして、今度は新機能提供側の話になるが、

新機能を追加する場合は、できれば、

新機能の初期値の挙動を従来の挙動に合わせた状態で(=デフォルトの挙動が追加前と変わらないようにして)

追加されるのが望ましい

これはHTML属性に限った話ではない。CSSプロパティにしろ、アプリの設定項目にしろ、もうこの世の全てに対して言える。

デフォルト値というのは、目に見えない。目に見えない値をもって加えられた変更(=暗黙の変更)は、検知しにくいため、問題の把握を遅らせる。非常に良くない。ユーザにとっては使いづらく、コーディングする人にとっても、今回のonloadバグのようにバグの原因にもなる。誰にも良いことがない。

ユーザ(今回の件で言えば、コーディング者もChromeとHTMLのユーザである)は、アプデ後も見えない部分は変わっていないと思い込んで使うため、暗黙なデフォルト挙動を変えられてしまったら、今回のloading属性の落とし穴のような面倒事が起こりやすくなるのだ。


CSSでの例も書こう。

今回こそCSSの話題ではなかったが、ことにCSSというのは、暗黙デフォルト値のオンパレードだ。何も指定しない場合line-heightはいくつとして扱われるのか。imgdisplayだって、デフォルトではinlineだ。だから、imgをブロック的に表示したい殆どの人にとって、必然的にimg{display:block}の指定が必要になる。さもなくば、「画像の下に余白が~」「vertical-alignをいじって!」とかいう学習上のロス(遠回り)が発生する。初心者泣かせにも程がある設計だ。CSS初期値一覧なチートシートなんていう記事もあるくらいだ。

ただ、CSSの学習過程で、これらのデフォルト値を一通り(なんとなくでいいから)覚えてしまえば、楽になる。というか、CSSというのは正直、初期値を変更していく設定記法で、むしろ、「初期値を制する者はCSSを制する」と言っても過言ではないくらいだ。

その後大事になってくるのは、デフォルトの挙動や初期値は、一度制定されたら恒久的に変更されないのが望ましい、ということだ。

つまり、ある日突然、imgdisplayの初期値がinlineからblockに変更されてはいけないのである。初期値は目に見えないため、解説書(ドキュメント等)が必須になる。そんな目に見えない値を勝手に変更するのは、過渡期に新旧解説書が混在して学習者を混乱させる上に、既習得者も覚え直しが必要となり、損失しかない。

ところで、思えばoverscroll-behaviorが登場した時。あれはそれほど混乱にはならなかった。何故、overscroll-behaviorには今回のloadingのような落とし穴が無かったのか。

それは、overscroll-behaviorの初期値"auto"の挙動が、overscroll-behaviorの無かった時代と同じ挙動だったからだ。

つまり、overscroll-behaviorを明記しない=その新機能が発動しない=これまでと同じというありがたい仕様だったからなのだ。

そのおかげで、われわれはoverscroll-behavior必要な時だけ呼び出せる便利な機能として平和に迎え入れることができたのである。素直な拡張、後方互換性のある拡張である。機能拡張の主(既存)・従(新規)の関係が変わらず、綺麗なのだ。

それに対し、今回のloading属性の導入がこのような面倒事になった理由はもうお分かりだろう。

それは、loading属性の初期値"auto"の挙動が、loading属性の無かった時代と異なるからだ。

つまり、loadingの無い時代に書かれた昔のHTML文書のimgタグが、これからはloading="auto"が指定されたものとして解釈されてしまう(ネットワーク環境によってはloading="lazy"な挙動をとりうる)。これは、後方互換性を崩しかねない拡張である。同じデータが異なる意味を持ち始めるのだ。

今回のChromeの設計に関しては、かなりしょうがない側面もあるが、

正直、上記の理由から、Chrome側の機能拡張の仕方に問題があった、と言わざるを得ない。


言語だけでなく、アプリでの例も書こう。

例えば、iOSをアプデしたら、「英単語のタイプミスを自動修正する」とかいう要らない機能が追加され、その機能が暗黙にオンにされた状態でアプデされている。つまり、OSのデフォルト挙動が暗黙に変わった状態でアプデされる。

ユーザは、その機能を不便と思ってオフにしたくても、暗黙である以上、どの設定項目が原因なのかを突き止めるのが難しい。自分自身でオンにしたわけではないからだ。誰かが設定項目を研究し、「iOSの迷惑機能のオフの仕方! ○○という設定項目の××をオフにすればいいよ」というHowTo記事でも書かない限り、ユーザはその迷惑機能をオフにする方法を知ることができない。好みの設定にたどり着けない。

Windowsのコントロールパネルなんかも同じだ。あれの設計の悪さと言ったらゲロものだが、あのコントロールパネル中の大量の項目のどれかがアプデのたびに暗黙に書き替わってると思うと、吐き気がする。新機能追加ではないが、これまで無いと思ってた余計な機能に初めて遭遇した時も、そのユーザにとっては新機能の発見という意味で追加だ。なんかウィンドウをドラッグして端に持っていったら勝手に最大化するやんけ、ウッザってなったなら、「ウィンドウ 勝手に最大化」とかでググって、誰かのHowTo記事でも見つけない限り、「コントロール パネルコンピュータの簡単操作コンピュータの簡単操作センターマウスを使いやすくしますウィンドウが画面の端に移動されたとき自動的に整列されないようにしますにチェック」とかいうクッソ分かりづらい解決策にはたどり着けない。何が「簡単」操作だ、よう言うわ

(Microsoftはその設計の悪さを認めたのか、それを「設定」という別アプリに移して再編する最中だが、そうすると今度は過去に蓄積されたHowTo記事が無駄になり、また1からHowToの作り直し。そもそも「コンパネ」から「設定」へ移行させること自体、デフォルトなものの書き替えだ。どの項目がどう対応し、どう移動し、どれがまだコンパネに残っているのかの情報がない点も、暗黙だ。初めから設計の良い設定画面を作っていればよかったものだ)

それに対し、iOS13の夜間モードは、良い例だ。夜間モードは、機能追加の周知のみが行われ、機能自体はデフォルトではオフの状態で追加された。そのお陰で、これまで通り使いたい人はこれまで通りの感覚で使い続けられるし、オンにしたいユーザは自分で選んでオンにできる。これが一番混乱が少なく、理想的なのだ。機能拡張の主(既存)・従(新規)の関係が変わらず、綺麗なのだ。


ただ、それでも新機能をデフォルトでオンにしたい、という人はいるだろう。

その理由の一つに、「新機能をまず使ってほしい」というものがある。

周知が失敗すれば、それは追加しなかったのと全く同じになり、せっかく作った新機能が勿体ないからだ。

まず使ってもらうことで、その新機能の存在だけでも知ってもらおう、という魂胆だ。

しかし、いい機能・求められる機能というのは、まず周知の時点で大いに期待され、周知が失敗することは少ない。初めは使われなかったとしても、本当にいい機能というのは誰かしらが布教ツイートしたりしていつの間にか支持されるものだ。夜間モードもその例だろう。

また、使われない新機能というのは、設計者は「まず試してもらうチャンスが少ないせいだ」と、使ってもらうチャンスの少なさのせいにしたがるが、実際には往々にして大多数のユーザにとって本当に必要ないから無視されている、ということがとても多いのだ。その新機能を「良くなった!」「便利になった!」「こんなの実装できたんだ!すごいだろ」と思ってるのは導入した本人やチームだけで、大多数のユーザはそんな機能なんてどうでもいいと思ってる場合が殆どだ。

実は、全く同じことは芸術でも起こり、例えば「こんな絵を描いたんだ!見てほしい!」「私の動画が再生されないのはまず見てもらうチャンスが少ないせいだ」と思ってるのは制作者だけで、実際には見に来た鑑賞者がその作品であまりいい体験を得られなかったからシェアされなかったのが事実、ということが多いのだ(制作者は勿論、自分の作品の完成度のせいにはしたくないため、チャンスの少なさのせいにする傾向がある)。本当に完成度の高い芸術作品というのは、少ないチャンスやきっかけで一気にファンが付くものだ。

つまり、作品を見てもらう/実装した機能を使ってもらえるチャンスの少なさではなく、結局は作ったものそのものの出来栄えで決まるところが大きいのだ。使ってもらうチャンスを増やそうとやたら新機能を無理やり使わせたがるのは、見てもらうチャンスを増やそうとしつこく自作品をリツイート(再掲)したり、「相互フォローしますから」とかいう臭いタグを使ってばかりの日々に明け暮れて、肝心の作品制作そのものに力を注がないでいるのと同じなのだ。いい作品は報われるべくして広まるし、いい機能は報われるべくして受け入れられ、定着する。これを考えると、やはり新機能はデフォルトでオフにすべき、という結論になるのだ。


※2019/10/17追記:

新機能デフォルトオンについて完全否定しているわけではなく、"暗黙の"新機能デフォルトオンのみを否定しているため、誤解を生まないための補足説明をコメント欄にて追記しました。


【余談】CSSのcaret-colorへの願い

「後方互換性のある綺麗な機能拡張」という話にちなんで、

個人的にCSS、およびcaret-colorへ抱いてる妄想を書く。


CSSでも、過去に多くの機能拡張(追加)が行われた。

例えば、昔text-decorationはそれ自体しか存在しなかった。text-decoration: noneでリンクから下線を消したり、text-decoration: line-throughで打消し線を引いたりするのに使われた。

しかし、今はこれが見事に後方互換性を維持したまま意味合いを変えて拡張されている。

今やtext-decorationは、下線なのか上線なのかの線の引き方を指定するtext-decoration-line・点線なのか実線なのかの線の種類を指定するtext-decoration-style・線の色を指定するtext-decoration-colorの3つのプロパティを一括指定するショートハンドプロパティ(一括指定プロパティ)として定義され直された。一括指定プロパティには他に、background-color等に対するbackgroundや、margin-left等に対するmarginがある。一括指定プロパティの下に子分のように連なる子プロパティたちは、「-line」「-style」「-color」「-left」のように、ハイフンで命名されている。

このような派手な拡張をしたにも拘らず混乱が起きていないのは、一括指定の書き方の仕様上、値に1つしか書かなくてもいい(触れなかった指定は暗黙に初期値となる)ため、例えばtext-decoration: line-throughはこれまで通り機能するというわけだ。

従来のtext-decoration: line-throughは、「text-decorationline-throughである」以上の意味を持たなかったが、

今のtext-decoration: line-throughは、「text-decoration-lineline-throughで、text-decoration-styleが初期値solidで、text-decoration-colorが初期値currentcolorである」という意味に変わるものの、初期値がうまいこと仕事して結果的に従来と同じ挙動になり、互換性が見事に維持されている。


ここで、caret-colorの話になる。

CSSには現在、caret-colorというプロパティがあり、キャレット(入力欄の点滅するやつ)の色を変えられる。初期値はautoで、普通は黒だ(厳密には、入力欄のテキストと同じ色になる)。

しかしこれ、すごく機能不足な気がする。設定できるの色だけなの?

初めから名前にハイフン入ってるが、caretというプロパティは2019年現在、CSSには存在しない。

というか、キャレットの種類には縦棒・下線・ブロックとかいくつか種類あるじゃん。

個人的には、その種類が指定できてほしいんですけど(というか、ブロックなキャレットの方がテキストエディタっぽくて好き)。

それに、現在の縦棒キャレットは、正直細くて見づらい。目が悪い人にとっては、ブロックキャレットの方が見やすいのでは?

ブロックキャレットをJavaScriptで疑似的に再現したコードは数あれど、

全角半角交じりの日本語フォントに対応できているものは何一つ無かった。

いずれも、日本語文字を入れたり消したりするごとに、どんどんキャレット位置がずれていくバグがある。

JavaScriptで無理があるなら、ブラウザ側がCSSでブロックキャレットを用意してくれればいいのだ。

これって… 超可能性秘めてない?

そう、ここまで読んだ君なら分かるだろう。

こいつはtext-decorationの後に続くべきだ、と。

こいつも後方互換性を維持した拡張がたやすくできるはずだろうに、と。


これが新しいCSSプロパティ、caretとcaret-styleだ!

※架空の妄想のため実在しない。ていうか何で実在しないのか理解できない。実在しろ()

caret: block red;      /* caret-styleとcaret-colorの一括指定。

他の一括指定プロパティ同様、片方の値を省くこともできる。 */

caret-style: block; /* 初期値はauto。多くのブラウザは、vertical-bar(縦棒)の値を使うだろう。
値は他に、block(ブロック)、underline(下線)、
outline(ブロックの枠線だけバージョン)等。 */

caret-color: red; /* 初期値はauto。既存のcaret-colorと同じく、キャレットの色指定。
但し、caret-style:block; 時、ブロック型キャレットの中に文字が入った場合、
キャレット内の文字色はどうするかという問題があるが、
それは素直に入力欄の背景色を透過させるのがいいだろう。 */

※他に caret-width:px値; caret-height:px値; とかも有りうるが、

 縦棒キャレットの幅を太くしてブロックっぽくした場合のブロックとの区別問題や、

 そもそも半角幅のブロックキャレットは全角文字に重なった場合その全角幅に合わせるのが普通だが

 絶対値指定だとその融通が利かず望ましくない等、問題点が多いため却下。

 (Windowsのキャレット幅設定がこれなんだよなぁ…)

caret-blink: none;でキャレットの点滅を止める、sms値で点滅速度、とかも考えたが、

 そもそも点滅の仕方まで制御するとかいうそんな小細工めいた需要は少ないと思われる。

 それでも、どうしてもキャレットの点滅を制御したい人のために、

 キャレットの点滅実装をCSSアニメーションと紐づけ、

 caret-animation: animationの値; とかを用意してあげてもいいかもしれない。

 ただ、そんなことすると点滅以外のアニメーションもできるようになってカオスなので、

 値を制限するか、やはり無しか、保留。議論の余地あり。


まとめ


  • Chromeでは、loading属性を全く書かずに従来通りimgiframeを書いたとしても、暗黙にデフォルトでloading="auto"が指定されたものとされ、ネットワーク環境次第で意図せずloading="lazy"の挙動を起こしてしまい、ページ全体のonloadがあてにならなくなってしまうという落とし穴がある。それを防ぐには、明示的にloading="eager"を指定せねばならない。


  • 今回のloadingの落とし穴を分析すると、教訓が判明した: 新しい機能や項目が登場した時は、何もしなかった場合の初期値・デフォルトの挙動を徹底的に把握しよう。初期値の設計が悪い場合、落とし穴があるかもしれないからだ。


  • また、新機能提供側にも教訓が判明した: 暗黙の仕様(挙動)変更は悪。異論は認めない。機能拡張や仕様変更をする時は、初期値を従来の挙動に合わせることで、暗黙の変更が発生しないように配慮すべし。実際、初期値のオンパレードなCSSは、これが上手く考慮された設計になっている。