HTMLImageElement.decode()
定義: HTMLImageElement.decode()
は、画像のデコード完了を Promise で通知する API。img.src
設定後に await img.decode()
で描画直前まで非同期に待機でき、reflow/paintのタイミング制御が可能。
なぜ重要か
- LCP候補の画像を確実にデコードしてからフェード/差し替えを行い、チラつきやレイアウトジャンプを回避。
- 画像のリサイズやフィルタ等のメインスレッド負荷を、描画フレームと衝突させない設計に寄与。
img.onload
だけではデコード完了を保証しない環境差を吸収。
実務での活用(判断基準付き)
- ヒーロー画像/LCP:
preload
+fetchpriority="high"
+await img.decode()
→requestAnimationFrame
で差し替え。 - 多数サムネ:
IntersectionObserver
で可視領域直前にdecode()
を予約。 - アニメ/フィルタ合成:
OffscreenCanvas
や Worker での前処理と組み合わせ、Long Tasks 50ms超を0本化できているかを指標に。
実装例
基本(LCP候補のフェード差し替え)
const img = new Image();
img.src = "/images/hero-1280.webp"; // preload 併用
try {
await img.decode(); // デコード完了待ち
requestAnimationFrame(() => {
const hero = document.getElementById("hero");
hero?.replaceChildren(img); // 描画はフレーム境界で
hero?.classList.add("is-ready"); // CSSアニメはprefers-reduced-motionに配慮
});
} catch {
// decode未対応/失敗時フォールバック
img.onload = () => requestAnimationFrame(() => {
document.getElementById("hero")?.replaceChildren(img);
});
}
可視直前の遅延デコード(サムネ一覧)
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
const img = e.target as HTMLImageElement;
(async () => {
try { await img.decode(); } finally { img.classList.add("ready"); }
})();
io.unobserve(img);
}
}
}, { rootMargin: "200px 0px" });
document.querySelectorAll<HTMLImageElement>(".thumb[data-src]").forEach((el) => {
el.src = el.dataset.src!;
io.observe(el);
});
検証と失敗例
- rAF外でのDOM差し替え: 描画と衝突してINPスパイク。
await img.decode()
→requestAnimationFrame()
の順序を守る。 - 巨大画像のリサイズを
decode()
後にメインスレッドで実行 → Long Tasks が発生。Worker/OffscreenCanvasへ移送。 decode()
非対応環境で例外未処理 → 画像が永遠に表示されない。try/catch
+onload
フォールバックを用意。sizes
/srcset
不整合 → 過大ダウンロードまたはぼけ。レイアウト幅と一致させる。
チェックリスト
- LCP候補は
preload
+fetchpriority=high
+decode()
+rAF
をセットで運用。 sizes
と実表示幅が一致しているか(Networkタブの実解像度で確認)。- 画像処理やフィルタはWorkerへ移送し、Long Tasks 50ms超を0本化できているか。
prefers-reduced-motion
でアニメの強度を落としているか。