🕰️ 「読み込み中」が目に見えていた時代
ふと思い出した話から始めます。
30年ほど前、インターネットが遅かった頃。画像が一気に表示されることはありませんでした。上から一行ずつじわじわ現れるもの。ブラインドが閉じるように粗い像から埋まっていくもの。全体がぼやけた状態からじわっとピントが合うもの。
当時は 28.8kbps のモデム(=電話回線でインターネットにつなぐ機械。ピーガガーという接続音のあれ)が主流で、100KB の画像 1 枚に 30 秒近くかかりました。テレホーダイ(=NTTの深夜定額サービス。23時から翌朝8時まで通話料が定額)の開始時刻を待ってから重いページを開く、という生活です。
で、思ったんです。
あれ、再現できるんじゃないか。
作りました。まず見てください。画面に入ると当時と同じように読み込まれます。見逃したら、下のボタンで何度でも。
これは GIF のインターレースという方式の再現です。以下、なぜこう見えていたのかを解きます。
🧩 あれは演出ではなく、データが届く順番そのものだった
いちばん大事なのはここです。あの見え方は「読み込み中アニメーション」ではありませんでした。 誰も演出として作っていない。
画像ファイルの中でデータが並んでいる順番が、そのまま画面に出ていただけです。ブラウザは「届いたぶんだけ描く」という素直なことをしていて、回線が遅かったから、その過程が人間の目に見えていた。
だから見え方が3種類あったのは、データの並べ方が3種類あったからです。
📜 上から一行ずつ — ベースラインJPEG / 非インターレースGIF
いちばん素直な並べ方です。画像を上の行から順番に、1行ずつ記録していく。
届いたバイトから順に描けば、当然こうなります。「今どこまで届いたか」が定規のように可視化されていたわけです。
ちなみにこの方式、待っている側からするとけっこう辛いものでした。目当ての画像かどうかが、半分以上ダウンロードするまで分からない。人の顔写真なら、髪の毛から順に出てくるので、顔が見えるまで待つしかない。
🪟 ブラインドが閉じるように — GIF89aのインターレース
そこで発明されたのがインターレース(=飛ばし飛ばしに記録する方式)です。GIF89a(=1989年に策定された GIF の規格)に入っています。
行を4回に分けて、間引きながら送ります。
| パス | 送る行 | この時点で | 見え方 |
|---|---|---|---|
| 1回目 | 0, 8, 16, 24… | 全体の 1/8 | 極端に粗いが、全体の構図はもう分かる |
| 2回目 | 4, 12, 20… | 1/4 | 少しマシに |
| 3回目 | 2, 6, 10… | 1/2 | だいぶ見える |
| 4回目 | 1, 3, 5, 7… | 完成 | くっきり |
ブラインドが閉じるように見えるのは、まだ届いていない行を、直前に届いた行で埋めているからです。1回目のパスで届いた 0 行目を、1〜7 行目にも縦に引き伸ばしてコピーする。だからあの独特の「縞が細かくなっていく」動きになります。
これは当時としてはかなり実用的な発明でした。全体像が最初の 1/8 で分かるので、「目当ての画像じゃないな」と判断して次のページに行ける。 回線が遅い時代の、まっとうなユーザー体験の工夫です。
PNG はこれをさらに進めて Adam7(=縦横の両方向に間引く7パス方式)を採用しています。名前は考案者 Adam M. Costello に由来します。
🌫️ ぼやけてから鮮明に — プログレッシブJPEG
3つ目がこれです。仕組みが他の2つとかなり違います。
JPEG は画像を DCT(=離散コサイン変換。画像を「なだらかな成分」と「細かい成分」に分解する処理)にかけて記録しています。空のグラデーションのようななだらかな変化が低周波、輪郭や細い線のような急な変化が高周波です。
プログレッシブ JPEG は、この低周波のほうから先に送ります。
最初に届くのは「だいたいの色と形」だけ。だからぼやけて見える。そこへ細部が乗っていって、最後にシャキッと決まる。
インターレースとの違いは、粗さの出方です。インターレースは縦方向にガタガタしますが、プログレッシブはピントが合っていないように見える。データの分け方が「行」なのか「周波数」なのかの差が、そのまま見え方の差になっています。
🛠️ どう作ったか
Canvas(=JavaScript で自由に絵を描ける HTML の要素)で実装しました。技術的な判断をいくつか書いておきます。
元の画像には手を出さず、canvas を上にかぶせる
既存の <img> を包むだけで動くようにしました。
<retro-image mode="interlace" duration="1400">
<img src="/photo.jpg" alt="..." width="960" height="540">
</retro-image>
canvas は position: absolute で img の上に重ねているだけです。JavaScript が動かなければ、ただの画像として普通に表示されます。
`drawImage` だけで完結させた
これが今回いちばん効いた判断です。
Canvas でピクセルを操作するときは getImageData という関数を使うのが普通なのですが、これを使うと canvas が tainted(=汚染された)状態になり、別のドメインから読み込んだ画像では例外を投げて止まります。CDN(=画像配信サーバー)に画像を置いている場合、追加のサーバー設定なしでは動かなくなる。
そこで、描画を drawImage(=画像の一部を切り取って、指定の位置に貼る関数)だけで組み立てました。
- 走査線: 元画像の上から N 行ぶんを切り取って、同じ位置に貼る
- インターレース: 1行だけ切り取って、8行分の高さに引き伸ばして貼る
- プログレッシブ: 縮小した画像を作って、元のサイズに拡大して貼る
全部「切り取って貼る」で表現できます。ピクセルを直接読まないので canvas は汚染されず、CORS の設定が要りません。
縞の粗さは「表示サイズ」で刻む
地味ですが大事な調整です。インターレースの「8行おき」を元画像の実ピクセルで刻むと、いまどきの 4000px ある画像では縞が細かすぎて見えません。当時の画像はせいぜい 300〜600px でしたから。
なので画面に表示されているサイズを基準に行を刻んでいます。忠実さより、当時の見た目の再現を優先しました。
⏳ 当時の待ち時間まで再現する
せっかくなので、回線速度を指定できるようにしました。
<retro-image mode="interlace" speed="28.8k">
こう書くと、画像の実際のファイルサイズから当時の所要時間を計算します。 実効スループットは bps ÷ 10 バイト毎秒で見積もっていて、28.8k なら約 2.8KB/s。当時の体感とだいたい合います。
ちなみに 56k を指定したときの値を 53000bps にしてあるのは、V.90 モデムの下り速度が、アメリカの規制で 53.3kbps に制限されていたからです。「56k」と名乗りながら 56k は出ませんでした。
なお、そのまま使うと 40KB の画像で 14 秒待たされます。実用にならないので、既定では 8 秒で打ち切る設定にしてあります。
下は打ち切りを外した、28.8k モデムそのままの設定です。勝手に始まると邪魔なので、押したときだけ動くようにしてあります。本当に 14 秒かかります。
🔁 30年後、同じ問題が「CLS」という名前で戻ってきた
当時、画像の <img> タグに width と height を書いておかないと、画像が届くたびにページのレイアウトがガタン、ガタンとずれました。 読んでいた行が突然下に飛んでいって、目で追い直す羽目になる。
この現象、いま名前がついています。CLS(Cumulative Layout Shift = 累積レイアウト変更)。Google が 2020年に Core Web Vitals(=Web サイトの品質指標)の一つとして発表したものです。
そして対策は、30年前とまったく同じ。width と height を書く。 それだけ。
同じ問題が新しい名前で戻ってきて、対策も変わっていない、ということです。
今回のコンポーネントは、この部分は再現していません。canvas を絶対配置で重ねているので、演出中も高さは 1px も動きません。
📦 置き場所
依存ライブラリなし、JavaScript ファイル 1 つ、MIT ライセンスです。この記事で動いているものが、そのまま実物です。
なお prefers-reduced-motion(=OS の「視差効果を減らす」設定)が有効な環境では自動再生しないようにしてあります。動きに酔いやすい人に無理やり見せるものではないので。
🎬 おわりに
作ってみると、当時のブラウザが何をしていたのかが具体的に分かりました。
ブラインドが閉じるあの動きは、「まだ届いていない行を、直前に届いた行で埋めている」だけだった。ぼやけて見えたのは、周波数の低いほうから届いていたから。当時はただ「そういうもの」として眺めていた画面の裏側が、コードにしてみると全部説明がつく。
役に立つかと言われると、正直そんなに立ちません。面白かったので作った、というだけの話です。