decoding="async" について詳しく調べてみる
はじめに
画像表示のパフォーマンス改善において、「decoding="async" をつけましょう」というのをよく見かけますが、おそらくほとんどの人がその実際の挙動を理解していない、あるいは誤った認識をしていると思います。今回詳しく調べる前の僕も含めて。
loading と decoding の違い
画像のパフォーマンス改善で decoding="async" のほかに、もう一つよく言及されるのが loading="lazy" です。decoding 属性について詳しく見る前に、まずは loading 属性との違いについて理解したほうがいいと思います。
loading とは、ブラウザがどのように画像を読み込むかを示す属性です。その名のとおり、「ダウンロード」と認識して問題ないと思います。
しかし、ダウンロードしただけでは画像は表示されません。デコード (復号化) する必要があります。そのデコードをどのようにするかのヒントを与えるのが decoding 属性です。
さらに、それぞれの取りうる値についても注目してほしい。
-
loadingの値-
eager(デフォルト値)
即時読み込み (直訳:待ちきれない、など) 。 -
lazy
遅延読み込み (直訳:のんびりとした、など) 。
-
-
decodingの値-
sync
同期。 -
async
非同期。 -
auto(デフォルト値)
ブラウザが最適な方法を決めます。
-
ここで注目してほしいのが、loading のほうは「即時」か「遅延」かです。「同期」か「非同期」かではありません。「loading="lazy" をつけると非同期読み込みになる」というのをたまに見かけますが誤りです。画像のダウンロード自体は元から非同期です。

上図は loading="eager" 状態の複数画像のダウンロードを示していますが、見てのとおり画像のダウンロードは非同期で行われ、かつメインスレッドをブロックしません。もちろん loading="lazy" の場合も同様です。
基本的に loading="lazy" は、ビューポート外にある画像に使うべきで、ファーストビュー内の画像に使ってしまうと、逆にダウンロードの開始が遅延してしまい、それによってページ全体の読み込み完了が延びてしまいます。
loading のほうは結構わかりやすいが、decoding の挙動は少々難解なので、少し詳しく見ていきましょう。
decoding についてよくある誤解
❌ 画像のデコードが終わるまで、その下のコンテンツは表示されない
decoding="sync" だと、画像のデコードが完了しないと、その下のコンテンツが表示されないと思われがちですが、事実ではありません。

上図は明示的に画像に対して decoding="sync" を指定していますが、画像のダウンロードが完了し、デコードが完了していないタイミングですが、下のテキストが表示されているのが確認できます。

画像が複数の場合も同様です。3、4枚目のデコードがまだ完了していないタイミングですが、下のテキストは表示されています。
❌ ダウンロードされた画像はすべてデコードされる
画像に loading="lazy" をつけていない場合、すべての画像がダウンロードされ、デコードもされると思われるかもしれませんが、実はビューポート近く以外の画像はデコードされません。

上図ではページ下部の画像に明示的に decoding="sync" をつけています。loading="lazy" ではないのですべてダウンロードされているのは確認できます。しかし、デコードはどこにも出てきません。画像近くまでスクロールすると、デコード処理は出現します。
decoding="async" と decoding="sync" の実際の挙動の違い
上述のとおり、decoding 属性はビューポート近くの画像にしか意味がありません。実際に sync と async の違いについて検証してみます。
左 decoding="sync" / 右 decoding="async"
上図を見てどう思いますか?違いはまったくないように見えます。DCL (DOMContentLoaded) 、L (Load) 、FP (First Paint) 、(First ContentfuL Paint) 、LCP (Largest Contentful Paint) 、どれもほぼ同じタイミングです。
左 decoding="sync" / 右 decoding="async"
複数枚のときも同様、Load のタイミングはどちらでも誤差が生じますが (1枚目で発火したり、4枚目で発火したりなど) 、sync と async 間の差は確認できませんでした。
この結果は正直意外でした。しかし、これでは decoding 属性はまったく意味ないじゃないか?
キャッシュがある場合
そしてその違いをついに見つけることができました。なんとキャッシュがあるときのみ、decoding="sync" と decoding="async" に顕著な違いが出ました。
左 decoding="sync" / 右 decoding="async"
まず、上図では画像のダウンロードが発生していないので、キャッシュを使っているのがわかると思います。このときに、decoding="sync" では FP、FCP、LCP はかならずデコードの完了後、一方 decoding="async" ではデコードと関係なしに処理されるのが確認できます。
結局 decoding="async" つけたほうがいい?
では、キャッシュがないときに両者に違いはなく、キャッシュがあるときは decoding="async" のほうが FP、FCP、LCP が非同期的に処理されるなら、やっぱり decoding="async" をつけたほうがいいんじゃないか?と思うかもしれません。しかし、話はそんなに単純ではありません。
左 decoding="sync" / 右 decoding="async"
画面表示に注目してみてください。decoding="sync" のほうは画像と一緒にページが表示されるのに対して、decoding="async" のほうは画像以外のコンテンツが先に表示されて、あとから画像が表示されます。後者のほうがいいように思いますが、これがキャッシュがあるときだけの話ということを忘れないでください。
左 decoding="sync" / 右 decoding="async"
さらに、実はこれまでは検証しやすいよう、かなり大きいサイズの画像を使っていましたが、上図を見るとわかるように、適切なサイズの画像にした場合は、このデコードにかかる処理はかなり小さいです。しかし、それがどんなに小さくても、キャッシュがあるときに decoding="async" にしていると、画像があとから表示されるというチラツキが必ず発生します、キャッシュがあるので基本的にページの表示がそこまで遅くならないはずなのに。これでは得られるメリットと割に合わなすぎます。
まとめ
検証した結果、これが本当に仕様通りなのかどうかはわかりません。しかし、現実として Chrome はこのように実装されています。
これまではわりと脳死で decoding="async" をつけてきましたが、今回の調査結果を受けて、今後は基本的に decoding 属性なし (decoding="auto") の方針で行きます。
ちなみに、キャッシュがあるときに auto でも基本的にチラツキは発生しません。場合によっては async と同じ挙動になることもあるかもしれませんが、基本方針としてはブラウザ任せにして、必要に応じて明示的に sync か async を選択することにします。
Discussion