GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

画像 遅延読み込み 9種 — プレースホルダー アニメーション

画像 遅延読み込みの9つのうち、1つはスクロール位置の値から交差比率を計算して画像をフェードインさせ、1つはクリックの値から読み込みタイミングを計算してiframeのプレースホルダーを実際の表示に変え、1つはアイドル時間の値から遅延時間を計算して重いウィジェットを後から読み込みます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

以下の9つのデモは、どれも本物の画像・動画・iframe・Webフォントをネットワーク越しに取得していません。ここで「読み込み」に見えているのはすべて、タイマーか、observerのコールバックか、アイドル時間の予算のどれかが代わりを務めているだけです。これらを実際のページに組み込むときは、プレースホルダーから実物へ切り替わるその瞬間こそが、本物の <img src> や fetch、本物の <iframe src>、実際の document.fonts.ready を差し込む場所になります。Math.random も外部ライブラリも一切使っていないので、同じパターンは何度実行しても同じ動きになります。

読み込む価値があるかどうかを決める、9つの小さな計算

このページの9つはすべて、同じ問いに違う入力で答えています——今がそれを読み込む価値のある瞬間かどうか。パターン03はスクロール位置に尋ね、パターン09はブラウザ自身のアイドル時計に尋ね、パターン07はそもそもクリックされたかどうかに尋ねます。各パターンのコードの上には、その判断を担っている関数の名前を書いているので、zipからファイルを取り出したあとも迷わず見つけられます。

01代替書体から本来のフォントへ切り替わる遅延読み込みテキスト

フォントが用意できたかどうかの値を計算して代替書体をまず表示し、本来のフォントが揃うと文字の形を自然に切り替えます。各文字には STAGGER_STEP_MS にインデックスを掛けた値で transitionDelay を個別に計算して与えるので、切り替えは一斉ではなく左から右へ波のように広がります。実際の実装では本物の document.fonts.ready や font-display: swap の境界に置き換える部分で、ここでは本物のWebフォントは一切ダウンロードしていません。

代替書体から本来のフォントへ切り替わる遅延読み込みテキスト
新しいタブで開く

01 · 代替書体から本来のフォントへ切り替わる遅延読み込みテキスト — 計測 累積面積 5.237% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STAGGER_STEP_MSFOUT_DELAY_MSRESET_DELAY_MS
  var TEXT = '遅延読み込み見出し';
  var STAGGER_STEP_MS = 45;
  var FOUT_DELAY_MS = 700;
  var HOLD_MS = 1800;
  var RESET_DELAY_MS = 900;
  var timer = null;

  // 計算レイヤー: 文字インデックスの値から切り替え遅延を計算し、スプリングオーバーシュートが順番に広がる。
  var chars = [];
  for (var i = 0; i < TEXT.length; i += 1) {
    var span = document.createElement('span');
    span.className = 'lff__char';
    span.textContent = TEXT[i] === ' ' ? ' ' : TEXT[i];
    span.style.transitionDelay = (i * STAGGER_STEP_MS) + 'ms';
    head.appendChild(span);
    chars.push(span);
  }

  function resetState() {
    root.classList.remove('lff--loaded');
    for (var i = 0; i < chars.length; i += 1) { chars[i].classList.remove('lff__char--loaded'); }
    badge.textContent = '代替フォント表示中';
  }

02画面幅の値が選ぶ背景の遅延読み込み

画面幅の値を計算して狭ければ低解像度、広ければ高解像度の背景を選び、プレースホルダーの色から実際の背景へ切り替えます。crossfadeIn は経過時間を全体時間で割って0〜1の進捗値を求め、その値からブラーと不透明度を毎フレーム書き換えます。実際に組み込む際は WIDTH_NARROW・WIDTH_WIDE を本物の画像URLに、プレースホルダーの色を background-image の読み込み完了イベントに置き換えてください。

画面幅の値が選ぶ背景の遅延読み込み
新しいタブで開く

02 · 画面幅の値が選ぶ背景の遅延読み込み — 計測 累積面積 38.076% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BREAKPOINT_PXCROSSFADE_MSWIDTH_NARROW
  var BREAKPOINT_PX = 240;
  var WIDTH_NARROW = 180;
  var WIDTH_WIDE = 300;
  var LOAD_DELAY_MS = 480;
  var CROSSFADE_MS = 480;
  var CYCLE_MS = 2200;

  var lastTier = null;
  var raf = null;
  var loadTimer = null;

  // 計算レイヤー: 経過時間の値を全体の遷移時間の値で割って進捗率を求め、
  // その進捗率でブラー半径と不透明度を毎フレーム再計算する。
  function crossfadeIn(el, duration) {
    var start = performance.now();
    if (raf) { cancelAnimationFrame(raf); }
    function step(now) {
      var progress = Math.min((now - start) / duration, 1);
      el.style.opacity = String(progress);
      el.style.filter = 'blur(' + (6 * (1 - progress)) + 'px)';
      if (progress < 1) { raf = requestAnimationFrame(step); }
    }
    raf = requestAnimationFrame(step);
  }

03スクロール位置の値が交差比率を計算する画像フェードイン

スクロール位置の値で画像がビューポートと重なる交差比率を計算し、その比率が基準を超えるとプレースホルダーから実際の画像へフェードインします。applyRatio は観測のたびにブラー半径とスケールを再計算するので、画像は一段階でではなくスクロールした分だけ少しずつくっきりしていきます。その基準点を早めたり遅めたりするのに触る行は INTERSECTION_THRESHOLD 一つだけです。

スクロール位置の値が交差比率を計算する画像フェードイン
新しいタブで開く

03 · スクロール位置の値が交差比率を計算する画像フェードイン — 計測 累積面積 36.04% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

INTERSECTION_THRESHOLDSTEPSapplyRatio
  var INTERSECTION_THRESHOLD = 0.4;
  var STEPS = [];
  for (var s = 0; s <= 20; s += 1) { STEPS.push(s / 20); }

  // 計算レイヤー: 交差比率の値から観測のたびにブラー半径・スケールを再計算し、
  // ビューポートと重なった分だけ画像がくっきりする。比率が基準を超えると読み込み完了として固定する。
  function applyRatio(card, ratio) {
    var img = card.querySelector('.lfv__img');
    var placeholder = card.querySelector('.lfv__placeholder');
    var blur = 10 * (1 - ratio);
    var scale = 0.92 + 0.08 * ratio;
    img.style.opacity = String(ratio);
    img.style.filter = 'blur(' + blur.toFixed(1) + 'px)';
    img.style.transform = 'scale(' + scale.toFixed(3) + ')';
    placeholder.style.opacity = String(Math.max(0, 1 - ratio * 1.4));
    if (ratio >= INTERSECTION_THRESHOLD) {
      card.classList.add('lfv__card--loaded');
    }
    if (card === cards[Math.min(cards.length - 1, Math.round(lastMaxIndex))]) {
      badge.textContent = '交差率 ' + Math.round(ratio * 100) + '%';
    }
  }

04名前の値がイニシャルを計算するアバターの遅延読み込み

入力した名前の値からイニシャル2文字を計算して丸いバッジでまず表示し、プロフィール写真が揃うとその場所を実際の写真に切り替えます。getInitials が先頭2文字を切り出し、別の関数が名前の文字コードの合計を PALETTE の色数で割った余りからバッジの色をハッシュで選ぶので、同じ名前は常に同じ色になります。実際のコードでは、プロフィール写真の取得が完了した瞬間にバッジを <img> に置き換えることになります。

名前の値がイニシャルを計算するアバターの遅延読み込み
新しいタブで開く

04 · 名前の値がイニシャルを計算するアバターの遅延読み込み — 計測 累積面積 1.731% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROW_STAGGER_MSLOAD_BASE_MSPALETTE
  var btn = document.getElementById('laiBtn');
  var NAMES = ['キム・ハヌル', 'イ・ドユン', 'パク・ソジュン'];
  var PALETTE = ['lai__row--a', 'lai__row--b', 'lai__row--c', 'lai__row--d'];
  var ROW_STAGGER_MS = 260;
  var LOAD_BASE_MS = 320;
  var CYCLE_GAP_MS = 2400;

  // 計算レイヤー1: 名前の文字列から先頭2文字をイニシャルとして切り出す。
  function getInitials(name) { return name.slice(0, 2); }

  // 計算レイヤー2: 名前の文字コードをすべて足し、その合計をパレットの長さで割った余りでバッジの色を選ぶ(ハッシュ)。
  function colorHashClass(name) {
    var sum = 0;
    for (var i = 0; i < name.length; i += 1) { sum += name.charCodeAt(i); }
    return PALETTE[sum % PALETTE.length];
  }

05格子位置の値が順番を計算するギャラリーの遅延読み込み

サムネイルの格子位置の値から表示遅延を計算し、左上から順に時間差を付けてプレースホルダーが実際の写真に切り替わります。delayFor はセルの行と列の値からマンハッタン距離の遅延を求めるため、表示は行ごとではなく斜めに扇状へ広がります。COLS が3で固定されているので、右下のセルだけが STAGGER_STEP_MS の4倍分、最後まで待つことになります。

格子位置の値が順番を計算するギャラリーの遅延読み込み
新しいタブで開く

05 · 格子位置の値が順番を計算するギャラリーの遅延読み込み — 計測 累積面積 20.573% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STAGGER_STEP_MSSTART_DELAY_MSdelayFor
  var COLS = 3;
  var ROWS = 2;
  var STAGGER_STEP_MS = 90;
  var START_DELAY_MS = 200;
  var CYCLE_GAP_MS = 1600;

  var cells = [];
  for (var i = 0; i < COLS * ROWS; i += 1) {
    var cell = document.createElement('div');
    cell.className = 'lgs__cell';
    cell.innerHTML = '<div class="lgs__placeholder"></div><div class="lgs__photo lgs__photo--' + (i % 4) + '"></div>';
    grid.appendChild(cell);
    cells.push(cell);
  }

  // 計算レイヤー: 格子位置(行の値+列の値)から遅延時間を計算する — マンハッタン距離のスタガー。
  // 左上(行0・列0)が最初に、右下に向かうほど遅く実際の写真に切り替わる。
  function delayFor(idx) {
    var row = Math.floor(idx / COLS);
    var col = idx % COLS;
    return START_DELAY_MS + (row + col) * STAGGER_STEP_MS;
  }

06再生の値が判定する動画ポスターの遅延読み込み

再生が始まったかどうかの値を計算して再生前はポスター画像だけを表示し、値が変わった瞬間に実際の動画フレームへ引き渡します。runBuffering はパターン02と同じ進捗率のループですが、ブラーではなく conic-gradient の角度に値を書き込むため、再生開始前にリングが目に見える形で一周し終えます。背後に本物の <video> や配信は一切なく、実際にはバッファリングの完了を待ってから要素を差し込む部分です。

再生の値が判定する動画ポスターの遅延読み込み
新しいタブで開く

06 · 再生の値が判定する動画ポスターの遅延読み込み — 計測 累積面積 21.261% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

BUFFER_DURATION_MSPLAY_HOLD_MSrunBuffering
  var BUFFER_DURATION_MS = 620;
  var PLAY_HOLD_MS = 1600;
  var CYCLE_GAP_MS = 700;

  var isPlaying = false;
  var raf = null;

  // 計算レイヤー: 経過時間の値をバッファリング全体の時間の値で割って進捗率を求め、
  // その進捗率をconic-gradientの角度変数に毎フレーム書き込んでリングが回る。
  function runBuffering(onDone) {
    var start = performance.now();
    frame.classList.add('lvp__frame--buffering');
    function step(now) {
      var progress = Math.min((now - start) / BUFFER_DURATION_MS, 1);
      ring.style.background = 'conic-gradient(from -90deg, #5b8cf8 ' + (progress * 360).toFixed(1) + 'deg, rgba(255,255,255,.18) 0)';
      if (progress < 1) {
        raf = requestAnimationFrame(step);
      } else {
        frame.classList.remove('lvp__frame--buffering');
        onDone();
      }
    }
    raf = requestAnimationFrame(step);
  }

07クリックの値が読み込み時点を計算するiframeの遅延読み込み

クリックの有無の値を計算してクリック前は地図・SNS埋め込みの場所に静的な案内だけを置き、クリックした瞬間だけその場所を実際のiframeに変えます。maxDistanceToCorner はクリック地点からフレームの四隅までの距離をピタゴラスの定理で求めて最大値を取るので、続くリップルはどこをクリックしてもフレーム全体を覆い切って終わります。実際に差し込む先は本物の <iframe src> で、このクリックが起きるまでは一切読み込まれません。

クリックの値が読み込み時点を計算するiframeの遅延読み込み
新しいタブで開く

07 · クリックの値が読み込み時点を計算するiframeの遅延読み込み — 計測 累積面積 21.261% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

RIPPLE_DURATION_MSLOAD_DELAY_MSmaxDistanceToCorner
  var RIPPLE_DURATION_MS = 520;
  var LOAD_DELAY_MS = 340;
  var CYCLE_GAP_MS = 2400;

  var loaded = false;

  // 計算レイヤー: クリック地点からフレームの四隅までの最大距離(ピタゴラスの定理)を
  // リップルの目標半径として計算し、実際のクリック地点から始まる円がフレームを覆う。
  function maxDistanceToCorner(x, y, w, h) {
    var dTL = Math.sqrt(x * x + y * y);
    var dTR = Math.sqrt((w - x) * (w - x) + y * y);
    var dBL = Math.sqrt(x * x + (h - y) * (h - y));
    var dBR = Math.sqrt((w - x) * (w - x) + (h - y) * (h - y));
    return Math.max(dTL, dTR, dBL, dBR);
  }

08スクロールの値が閾値を計算する下部セクションの遅延読み込み

スクロールの値が定めた閾値を超えたかを計算し、超える前は空のままにしていた下部セクションを超えた瞬間だけ実際の内容で満たします。checkThreshold はスクロールコンテナの scrollTop を SCROLL_THRESHOLD_PX と毎回のスクロール・ホイールイベントで比較し、超えた瞬間だけ双方向に状態を切り替えます。実運用ではその同じ境界で、空のセクションを fetch やフレームワークの遅延読み込みルートに置き換えることになります。

スクロールの値が閾値を計算する下部セクションの遅延読み込み
新しいタブで開く

08 · スクロールの値が閾値を計算する下部セクションの遅延読み込み — 計測 累積面積 16.765% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SCROLL_THRESHOLD_PXAUTOSCROLL_STEP_PXcheckThreshold
  var SCROLL_THRESHOLD_PX = 86;
  var AUTOSCROLL_STEP_PX = 2.2;
  var CYCLE_GAP_MS = 1400;

  var filled = false;
  var userActive = false;

  // 計算レイヤー: スクロールコンテナのscrollTopの値を決めた閾値(px)と比較し、
  // 超えた瞬間だけfilled状態に切り替わる(超える前は空のまま)。
  function checkThreshold() {
    var over = view.scrollTop >= SCROLL_THRESHOLD_PX;
    if (over && !filled) {
      filled = true;
      section.classList.add('lsr__section--filled');
    } else if (!over && filled) {
      filled = false;
      section.classList.remove('lsr__section--filled');
    }
  }

09アイドル時間の値が遅延時間を計算するウィジェットの遅延読み込み

ブラウザが空いているアイドル時間の値を計算し、その時間が確保できるまで重いおすすめ・広告ウィジェットの場所をプレースホルダーのままにしておきます。tick は requestIdleCallback が返す残り時間を呼び出しのたびに積み上げて IDLE_BUDGET_MS に達するまで続け、ブラウザがアイドル時間を報告しない場合は上限付きの setTimeout にフォールバックします。この予算に達してから差し込むという流れこそ、本物の広告枠やおすすめウィジェットのスクリプトを引き渡す位置です。

アイドル時間の値が遅延時間を計算するウィジェットの遅延読み込み
新しいタブで開く

09 · アイドル時間の値が遅延時間を計算するウィジェットの遅延読み込み — 計測 累積面積 17.406% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

IDLE_BUDGET_MSIDLE_TIMEOUT_MSIDLE_TICK_CAP_MS
  var accumulated = 0;
  var idleHandle = null;
  var running = false;

  // 計算レイヤー: requestIdleCallbackが渡す残り時間(timeRemaining)の値を
  // コールバックのたびに積み上げ、積み上げた値が目標予算(IDLE_BUDGET_MS)に
  // 達した瞬間だけウィジェットの場所を実際の内容に切り替える。
  function tick(deadline) {
    if (!running) { return; }
    var got = deadline.didTimeout ? IDLE_TICK_CAP_MS : Math.min(deadline.timeRemaining(), IDLE_TICK_CAP_MS);
    accumulated = Math.min(IDLE_BUDGET_MS, accumulated + Math.max(got, 12));
    var pct = Math.round((accumulated / IDLE_BUDGET_MS) * 100);
    bar.style.width = pct + '%';
    sub.textContent = 'アイドル時間確保中 ' + pct + '%';
    if (accumulated >= IDLE_BUDGET_MS) {
      widget.classList.add('liw__widget--loaded');
      running = false;
    } else {
      idleHandle = ric(tick, { timeout: IDLE_TIMEOUT_MS });
    }
  }

css 遅延読み込み という言葉で検索した人が実際に欲しいもの

css 遅延読み込み を調べている人が本当に知りたいのは、どの画面でどれを使うかという話であることが多いので、9つのうち3つを名指しします。パターン02の画面幅で選ぶ背景は、ヒーローセクションやイベントページにそのまま当てはまります——ページの他の要素より先に、プレースホルダーの色から適切な解像度の背景へ落ち着く様子を訪問者が最初に目にするからです。パターン07のクリックで読み込むiframeは、店舗位置の地図やSNS投稿の埋め込みにぴったり合います。この2つはページの下の方で使われないまま終わることが多い場所で、クリックされるまで地図タイルも埋め込みスクリプトも一切読み込まれません。

パターン09は、多くのページが見落としがちな場所にちょうど収まります——サイドバー広告や関連商品のおすすめ枠は、放っておくと最初の描画でページ本体の内容とブラウザの注意を奪い合ってしまいます。requestIdleCallback にそのウィジェットを読み込む価値があるかどうかを任せるのは、まだ誰も見ていない場所にパターン02や07と同じ考え方を当てはめているだけです。

どの値が本当に決めているのか、パターンごとに見比べる

パターン 見ている値 再計算の方法 デモの操作入力
01 フォント準備状態 切り替えごとに1回の setTimeout click, keydown
02 画面幅 requestAnimationFrame ループ click, keydown
03 スクロールで動く交差比率 IntersectionObserver コールバック scroll, wheel, touchstart
04 入力された名前の文字列 行ごとに1回の setTimeout click
05 格子の行+列の位置 セルごとの段階的な setTimeout click, keydown
06 再生開始フラグ requestAnimationFrame ループ click
07 クリック座標 requestAnimationFrame ループ click, keydown
08 コンテナの scrollTop scroll/wheelイベント+requestAnimationFrame scroll, wheel, touchstart
09 積み上げたアイドル時間 requestIdleCallback、setTimeout フォールバック click

多くの html 画像 遅延読み込み の解説はCSSのトランジションで止まってしまい、loadedクラスを「いつ」付け加えるかを決めるコードのほうには触れません。実はそこの判断こそがこの9つを分けている本当の違いで、フェード自体はどのパターンでも opacity と filter の1行にすぎません。

毎フレーム値を計算し直す場所 — 02・06・07・08

02・06・07は全く同じ進捗率のループ——経過時間を時間で割って1でクランプする——を共有していて、それをブラーと不透明度・conic-gradientの角度・リップルの半径という3種類の出力にそれぞれ書き込んでいます。08で requestAnimationFrame を使っているのはデモ自体の自動スクロールのためで、閾値の判定そのものはスクロールイベント上の単純な比較です。

一度きりのタイマーで決まる場所 — 01・04・05

01・04・05は requestAnimationFrame に一切触れません。文字・行・セルごとに1回だけ setTimeout が発火し、状態の間の動きはCSSトランジションに任せています。見た目の変化が連続的に計算し直す必要のない、一つのプロパティの一回きりの切り替えであるときは、こちらのほうが軽い方法です。

ブラウザ自身の合図で決まる場所 — 03・09

03と09は、自分で書いたタイマーではなくブラウザ側に判断を委ねている2つです。IntersectionObserver が比率を返し、requestIdleCallback が残り時間を返し、どちらのパターンも返ってきた数値にただ反応しているだけです。

出荷前に動かしておきたい3つの数値

バリエーション 項目 定数 基準 → 変更後 何が変わるか
03 画像フェードイン INTERSECTION_THRESHOLD 0.4 → 0.25 ビューポートとの重なりが小さいうちに、より早く読み込み完了と判定される
05 ギャラリースタガー STAGGER_STEP_MS 90 → 160 格子を斜めに広がる表示が終わるまで、はっきり長くかかる
09 アイドルウィジェット IDLE_BUDGET_MS 260 → 420 切り替わるまでに必要なアイドル時間の合計が増え、より多くのコールバックを経て待つ傾向になる

この3つのバリエーションはいずれもzip内の独立した index.html で、変わっているのはその定数一つだけです——リスナーやreduced-motionの分岐、その他のマークアップは基準パターンと変更後の間で何も変わりません。

9つとも共通して壊れる、ある一つの境界

この9つはどれも、自分の決めたタイミングどおりに最後まで進める前提で書かれていて、その途中で訪問者に割り込まれると同じところで壊れます——パターン03の比率がまだ上がっている最中にスクロールを止めて離れたり、パターン07のリップルが終わる前にiframeをもう一度クリックしたりすると、進行中の requestAnimationFrame やタイマーを自分でキャンセルできているかどうかが問われます。ここに含めた9つはすべて、新しい raf = の代入の前に cancelAnimationFrame を呼ぶか、コールバックの本体が動く前に loaded・filled・running のようなガード用フラグを確認していますが、この一手間こそ、誰かがパターンの見える部分だけをコピーして新しいプロジェクトに貼り付け、後片付けの行を省いたときに真っ先に抜け落ちる部分でもあります。

以下のzipには9つのindex.htmlと3つのバリエーションすべてが入っていて、パスワード 794sbpwn は画面を見ながら手入力してください。答えも一緒に映っているページからコピーしたパスワードでは、わざわざ入力させる意味がなくなってしまいます。

reduced-motion が消す、3つの動き

9つのパターンはすべて、読み込み時に一度だけ window.matchMedia('(prefers-reduced-motion: reduce)').matches を読み、その結果で分岐します。中でもアニメーションの過程が長い3つを見ると、その違いがいちばんよくわかります。パターン01は文字ごとの段階的な切り替えを飛ばし、左から右へ波打たせる代わりに全ての文字を一度に読み込み済みの状態にします。パターン03は applyRatio が普段行うブラー・スケールの推移を飛ばし、比率が基準を最初に超えた瞬間に完全にくっきりした画像へ直接切り替えます。パターン09の進捗バーは、アイドルコールバック一回ごとの積み上げをアニメーションで見せる代わりに、最終的な幅まで一段階で埋まります——ただし裏側のアイドル時間の計算自体は変わらず、reduced-motionが消しているのはあくまで判断の「見せ方」であって、判断そのものではありません。

画像 遅延読み込み javascript の実装がこれを一つも切らないままだと、OSレベルで動きを止めるよう申告した訪問者に対しても、そのままアニメーションを流し続けることになってしまいます。

この9つを試す前に、よくある疑問に先に答えておきます

項目05と09の動いたフレームが23枚中こんなに少ないのはなぜですか

どちらも一続きの場面ではなく、一度きりの読み込み遷移だからです。05は斜めのスタガーを、09は予算の積み上げを、測定期間が終わるよりずっと前に終えてしまうので、その後の23フレームの大半は同じ落ち着いた読み込み済みの状態を映しており、これはどちらのパターンの欠陥でもなく正しい挙動です。

html 画像 遅延読み込み を最初に試すならどれがいちばん簡単ですか

パターン01の代替フォント切り替えが、この9つの中でいちばん簡単な html 画像 遅延読み込み の例です——setTimeout を1回、CSSクラスの切り替えを1回、文字ごとに計算した transitionDelay だけで、あとのobserverやアイドルコールバック主導のパターンに進む前の入り口として向いています。

この9つの中で、実際にURLから画像・動画・スクリプトを読み込んでいるものはありますか

ありません。プレースホルダーから実物への切り替えはすべてタイマー・observerのコールバック・アイドル時間の予算のいずれかでシミュレートしていて、そこを本物の <img src>・<video>・<iframe src>・fetch に差し替える位置は、それぞれのパターンの説明文の中で名指ししています。

Sources: MDN — Intersection Observer API · MDN — Window.requestIdleCallback · MDN — img loading属性 · MDN — FontFaceSet · MDN — prefers-reduced-motion

Related reading: 無限スクロールローディング UIパターン9選 — スピナーから仮想ウィンドウまで · スケルトン チャート 9種 — 数式で完成する瞬間まで · 【コピペOK】シェアボタン デザイン 9選 — 埋め込みウィジェット — 1つ目はスピナーやスケルトンを実際の行に置き換える点で本物のデータを取得しない同じ原則を守っており、2つ目はパターン05が格子位置からスタガーを計算するのと同じように、実測したデータからスケルトンの表示タイミングを計算しており、3つ目はこの記事のパターン07とほぼ同じ考え方——クリックされるまで埋め込みの代わりに静的なプレースホルダーを置く——を共有しています。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。