フロントエンドエンジニアの皆さん、日々のパフォーマンスチューニングお疲れ様です。Core Web Vitalsのスコアに一喜一憂し、メインスレッドのJank(カクつき)と格闘する日々を送っていることでしょう。
さて、プロダクトの改修中、こんなシーンに出くわしたことはありませんか?
「LCP(Largest Contentful Paint)を改善したいんだけど、ファーストビューの巨大なヒーロー画像のせいでメインスレッドがブロックされてる……」。
画像サイズを圧縮し、WebPやAVIFを導入し、`fetchpriority=”high”`も仕込んだ。それでもなお、低スペックなモバイル端末でスクロールした瞬間や、大量のサムネイルが並ぶ一覧ページを表示した瞬間に、画面がカクっとフリーズする。
その原因、もしかすると「画像のデコード(Decoding)」がメインスレッドを暴行しているからかもしれません。
今回は、HTML5の隠れた名機能であり、ブラウザの内部挙動をコントロールするための強力な武器である、`img`タグの `decoding` 属性について、ブラウザの裏側の泥臭い仕組みから実務での使い分けまで、徹底的に解説していこう。
—
1. なぜ「画像のデコード」がメインスレッドの敵なのか?
まず、ブラウザが画像を表示するまでのライフサイクルを正確に把握しておこう。ここはフロントエンドエンジニアとして絶対に知っておくべき基本だ。
1. ネットワーク取得: HTMLがパースされ、``タグを見つけたブラウザが画像データをダウンロードする。
2. デコード(復元): 圧縮されたバイト列(JPEG, PNG, WebPなど)を、GPUやCPUが画面に描画できる「生のマッピングデータ(ビットマップ)」へと変換する。
3. ラスタライズ・描画: 画面上のピクセルにピクセルデータを展開する。
問題は「2. デコード」だ。
このデコード処理、実は見た目の印象以上にCPU負荷が高い重作業である。特にスマホのRetinaディスプレイのような高解像度環境では、数メガピクセルの画像をメモリ上で展開するため、数ミリ秒から数十ミリ秒のCPU時間を食いつぶす。
同期デコード(Sync)の悲劇
デフォルトの状態では、ブラウザはこのデコード処理を同期(Synchronous)で行おうとする。
つまり、DOMツリーに画像が組み込まれ、レイアウトが計算されるプロセス(Main Thread)の途中で、「おい、ちょっと画像データを展開しろ!」と割り込みが入り、その処理が終わるまでメインスレッドが完全にブロックされてしまうのだ。
これが、ページを開いた瞬間や、無限スクロールで新しい画像がDOMに挿入された瞬間に発生する「カクつき(Jank)」の正体である。UIスレッドがロックされ、ユーザーのタッチ操作やアニメーションが一時停止してしまう。
この厄介なボトルネックをコントロールするために生まれたのが、`decoding` 属性だ。
—
2. `decoding` 属性が提供する3つの選択肢
`` タグの `decoding` 属性には、以下の3つの値を指定できる。ブラウザに対して「どうやってデコードしてほしいか」を伝えるヒント(Hint)だ。
- `async`: 非同期デコード。他のコンテンツの描画をブロックせず、バックグラウンドスレッドでデコードを実行する。
- `sync`: 同期デコード。DOMへの挿入と同時にメインスレッドでデコードを完了させる。
- `auto`: デフォルト。ブラウザ自身に判断を委ねる。
ここで重要なのは、これらはあくまで「ブラウザへのヒント」であって強制命令ではないという点だ。しかし、現代のモダンブラウザ(Chromium、Safari、Firefox)は、このヒントを非常に賢く解釈して挙動を変えてくれる。
`async` と `sync` の内部挙動の違い
- `decoding=”sync”` の世界
画像が読み込まれ、DOMにアタッチされた瞬間にメインスレッドを占有してデコードを完了させる。そのため、画面に表示されるまでの遅延は理論上ゼロになるが、メインスレッドをブロックするため、他の描画やインタラクションが犠牲になる。
- `decoding=”async”` の世界
画像が読み込まれた後、デコード作業をバックグラウンドのスレッドプールへ投げ捨てる。メインスレッドはブロックから解放されるため、UIの滑らかさが保たれる。ただし、デコードが終わるまでのごくわずかな間、画像部分が白飛びしたり、プレースホルダーのまま描画が1フレーム遅れる可能性がある。
—
3. 実務におけるベストプラクティス:どう使い分けるべきか?
「じゃあ、全部 `async` にすればパフォーマンス最強じゃん!」と思ったそこの君、ちょっと待ってほしい。実務の世界はそんなに甘くない。間違った場所で `async` を使うと、逆にUXを破壊することになる。
シニアエンジニアとして、チームメンバーには以下の指針で使い分けるよう指導してほしい。
ケースA: ファーストビューの「LCP候補(ヒーロー画像など)」
- 推奨値: `decoding=”sync”` (または明示的な指定なしのデフォルト)
- 理由: LCP要素に対して `decoding=”async”` を指定すると、デコードが遅れる分だけ「画像が実際に画面に表示されるタイミング」が数フレーム遅延し、LCPのスコアが悪化するリスクがある。LCP画像は一刻も早く画面に出したいので、メインスレッドを多少犠牲にしてでも同期的にデコードさせるのが鉄則だ。
ケースB: ファーストビュー以外(スクロールした先にある画像、カルーセルの2枚目以降など)
- 推奨値: `decoding=”async”`
- 理由: ユーザーの視界に入っていない、あるいはメインのコンテンツではない画像は、一刻を争うものではない。メインスレッドをブロックさせないために、積極的に `async` を指定して裏でこっそりデコードさせておくべきだ。
ケースC: 無限スクロールのサムネイルや、ECサイトの商品一覧(数から勝負する画像群)
- 推奨値: `decoding=”async”` + `loading=”lazy”`
- 理由: これらはコンボ技として最強だ。`loading=”lazy”`でネットワークの帯域とロードタイミングを遅延させつつ、画面手前でロードされた瞬間に `decoding=”async”` でバックグラウンドデコードを走らせる。これにより、大量の画像を一度に読み込んでも、スクロール中のカクつきを劇的に抑え込むことができる。
—
4. すぐに使える!実践的コンポーネントコード例
実際のプロジェクトでどのように落とし込むか、ReactやVueなどのモダンなUIライブラリでも応用しやすい、きれいなHTML/CSSのパターンを見てみよう。
Webブラウザの仕組みを知る:decoding属性の極意

ここに記事のリード文や導入が入ります。このテキストがスムーズに描画されることがUX上非常に重要です。
—
5. チーフアーキテクトからの実践的アドバイス
最後に、実務でパフォーマンスチューニングを行う際の心構えを伝授しよう。
パフォーマンス最適化のツール(LighthouseやChrome DevToolsのPerformanceパネルなど)を叩くと、スコアや警告に一喜一憂しがちだ。しかし、ブラウザのアーキテクチャを理解していれば、「なぜその警告が出ているのか」「本当にその対応が必要なのか」が見えてくる。
`decoding=”async”` は魔法の杖ではない。LCP画像に誤って設定すれば、かえって指標を悪化させる劇薬にもなる。しかし、「どこがボトルネックで、ブラウザに何をさせたいのか」をエンジニア側が意図してコントロールできるようになれば、フロントエンドのクオリティは一段階上のステージへと引き上げられる。
次のスプリントでのチケットに、ぜひ画像の `decoding` 属性の見直しを組み込んでみてほしい。ユーザーのスクロールが滑らかになったことを、きっと数字が証明してくれるはずだ。

コメント