模写上級 #002 | 写真館サイトで学ぶ画像最適化


模写上級 #002 は、写真館のサンプルサイト「Lumiere Studio」を写しながら、写真を大量に並べるページの組み方——画面幅ごとの画像の出し分け・読み込ませる順番・表示比率の固定——を身につける課題です。見た目を似せるだけなら中級までで足ります。この課題が上級なのは、幅を変えても崩れないこと読み込み中に文字が飛ばないことまで含めて合格にするからです。

難易度上級
所要時間目安6〜8時間(見本の実装量から見積もった目安で、計測値ではありません)
使う技術HTML / CSS Grid / picture・srcset / object-fit・aspect-ratio
作るもの写真館の1ページ完結型サイト(7セクション)

写す対象の構成を先に分解し、画像まわりで手が止まりやすい4つの手順を順に扱います。最後にブラウザのコンソールで合否が出せる完成条件を置いてあるので、「なんとなく似ている」で終わらせずに済みます。仕様の出典は MDN と web.dev から2026年8月2日に取得しました。


差分チェックツールで効率UPお手本コードと自分のコードを比較して、違いを一目で確認できます。練習前にブックマークしておくと便利です。
Diff Checkerを開く →

この課題で作るもの|写真館サイト「Lumiere Studio」

写す対象は次のページです。ブラウザで開き、幅を変えながら眺めるところから始めてください。

模写サンプルサイト Lumiere Studio – Photography(advanced002)

模写上級シリーズの難易度を示すバッジ画像

上の画像は上級シリーズであることを示すバッジで、完成見本ではありません。完成見本は先ほどのサンプルサイトそのものです。ページは7つのブロックでできています。

セクション主な中身画像まわりの要点
ヘッダーロゴと4項目のナビ。768px未満はハンバーガー画像なし
ヒーロー画面いっぱいの写真に見出しを重ねる最初に見える画像。遅らせない
コンセプト左に文章、右に縦位置をずらした写真2枚縦長の比率を保つ
ギャラリー写真6枚。1枚は縦2マス、1枚は横いっぱいマスの数え方とトリミング
プラン写真つきの料金カード3枚3枚とも同じ比率に揃える
CTA背景写真の上にボタン2つ背景も1枚の画像
フッターロゴ・ナビ・住所・SNSアイコンアイコンはインラインSVG

見本を丸写しするだけでは足りない理由

見本サイトを実ブラウザ(Chromium・2026-08-02)で開いて数えたところ、13枚ある img のうち width と height を書いてあるものは0枚、srcset も0枚、picture 要素は0個、loading 属性も0枚でした。見本はレイアウトの手本であって、画像の指定の手本ではありません。

そこでこの課題の到達点は、見本と同じ見た目を作ったうえで、見本には書かれていない画像の指定を自分で足すところに置きます。写真素材は見本と同じ Unsplash の URL をそのまま使えます。


手順1|画面幅で画像を出し分ける(picture と srcset)

出し分けの道具は2つあり、用途が違います。MDNの picture 要素のリファレンス(2026-08-02取得)は picture の用途を「アートディレクション(幅ごとに違うトリミングを出す)」と「対応していない形式の代替」とし、高解像度ディスプレイ向けの出し分けだけが目的なら picture ではなく img の srcset を使うことを明記しています。

形式の切り替えは picture の出番です。source を上から順に見て、対応していない形式は飛ばされ、最後の img が受け皿になります。

<picture>
  <source type="image/avif" srcset="gallery-01.avif">
  <source type="image/webp" srcset="gallery-01.webp">
  <img src="gallery-01.jpg" alt="ウェディング撮影" width="800" height="1000">
</picture>

alt・width・height は picture ではなく 中の img に書きます。picture は入れ物にすぎず、選ばれた画像は img の場所に表示されるためです。

sizes を省くと大きすぎる画像が来る

同じ絵をサイズ違いで用意するときは img の srcset を使い、幅記述子(400w のような書き方)と sizes をセットにします。MDNの img 要素のリファレンス(2026-08-02取得)によると、sizes を書かなかった場合の既定値は 100vw、つまり「画面幅いっぱいで表示する」とブラウザに伝えたことになります。

実際に確かめました。400w と 1200w を srcset に並べ、CSSで表示幅を200pxに固定した img を、ビューポート幅1400px・端末ピクセル比1で開きます。sizes なしでは 1200w が選ばれ、sizes=”200px” を足すと 400w が選ばれました。小さく並べる画像ほど、この差が無駄な通信になります。

<img src="gallery-01-800.jpg"
     srcset="gallery-01-400.jpg 400w,
             gallery-01-800.jpg 800w,
             gallery-01-1600.jpg 1600w"
     sizes="(max-width: 767px) 50vw, 33vw"
     width="800" height="800" alt="ファミリー撮影">

sizes はギャラリーの段組みと同じ考え方で書きます。767px以下は2列なので画面のおよそ半分、768px以上は3列なのでおよそ3分の1、という対応です。sizes が効くのは幅記述子(w)を使ったときだけで、2x のような密度記述子とは混ぜられません。詳しくは MDN の レスポンシブ画像ガイド(2026-08-02取得)にあります。


手順2|遅らせてよい画像と、遅らせてはいけない画像

img の loading=”lazy” は、その画像が表示領域に近づくまで読み込みを先送りする指定です。写真を大量に並べるこの課題では効果が大きい反面、付ける場所を間違えると逆に表示が遅くなります

web.dev の 遅延読み込みの解説(2026-08-02取得)は「表示領域に入っている可能性が高い画像、とくにLCP画像を遅延読み込みしないこと」と注意し、LCP最適化の記事(同日取得)は「LCP画像を決して遅延読み込みしてはいけない。必ず不要な読み込み遅延を生む」とさらに強く書いています。同記事は fetchpriority=”high” を使う手も挙げつつ、高優先度にする画像は1〜2枚までにすべきだとしています。

この課題では、ヒーローの背景写真には loading を付けず(=既定の即時読み込みのまま)、ギャラリーの下のほう・プランのカード・CTAの背景写真には lazy を付けるという切り分けになります。境目は「最初の画面に見えているかどうか」です。


手順3|読み込み中に文章を動かさない(CLS対策)

画像が届いた瞬間に下の文章が押し下げられる現象を、レイアウトシフトと呼びます。Core Web Vitals は現在 LCP・INP・CLS の3つで、視覚的な安定性を測るのが CLS です。web.dev のCLS解説(2026-08-02取得)は良好の基準を0.1以下、0.25超を不良としています。応答性の指標は2024年3月12日に FID から INP へ置き換わっているので、古い記事の FID は現行の指標ではありません(出典:web.dev の切り替え告知・同日取得)。

width と height を書くと何が起きるか

MDNの img リファレンスには、height と width を書くと読み込み前にブラウザが縦横比を計算でき、その比率で必要な場所を先に確保するため、レイアウトシフトを減らす、あるいは防げると書かれています(2026-08-02取得)。CSS側で height: auto にしておくのが前提です。

効き目を数字で確かめました。800×600の画像をわざと0.8秒遅らせて返すテストページを、390×844のビューポートで開いた結果です。

画像の書き方実測CLS判定
width・height なし0.4941不良(0.25超)
width=”800″ height=”600″ を追加0良好
属性なし・親要素にCSSで aspect-ratio0良好

3行目のとおり、枠の大きさがCSSで先に決まっていれば属性が無くてもシフトは起きません。見本サイトを同じ方法で測ると0.0033で、枠を aspect-ratio と固定高さで決めているためほぼゼロでした。それでも属性を書く価値があるのは、CSSが枠を決めていない画像が残るからです。両方やっておくのが安全側です。

自分のページのCLSは、開発者ツールのコンソールに次を貼れば測れます。buffered: true があるので、読み込み後に貼っても記録済みのシフトを拾えます。

let cls = 0;
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (!e.hadRecentInput) cls += e.value;
  }
  console.log('CLS', cls.toFixed(4));
}).observe({ type: 'layout-shift', buffered: true });

手順4|比率を固定してトリミングする

写真の縦横比はバラバラでも、並べたマス目は揃っていてほしい。両立させるのが aspect-ratio で枠を作り、object-fit で中身を合わせるやり方です。MDNの object-fit のリファレンス(2026-08-02取得)によると、object-fit は img や video のような置換要素の中身の収め方を決めるプロパティで、既定値の fill は枠に合わせるために中身を引き伸ばします

16:6の枠に4:3の写真を入れて確かめると、object-fit を書かない状態では枠の縦横比が2.667、元画像は1.333——横に約2倍引き伸ばされていました。cover を指定すると、はみ出した部分が切り取られて比率のズレは消えます。位置の調整は object-position で行い、どちらも picture ではなく img に指定します。

ギャラリーの骨格は次の形です。1マスを正方形にしておき、大きく見せたいカードだけまたぎ方を変えます。

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}
.gallery-item {
  aspect-ratio: 1;
  overflow: hidden;
}
.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (min-width: 768px) {
  .gallery-grid { grid-template-columns: repeat(3, 1fr); }
  .gallery-item--large { grid-row: span 2; aspect-ratio: auto; }
  .gallery-item--full  { grid-column: span 3; aspect-ratio: 3 / 1; }
}

大きいカードで aspect-ratio: auto に戻すのは、縦2マスぶんの高さを行に任せるためです。実測でも1400px幅で大きいカードが841px、通常カードが408px(=408×2+隙間24)となり、意図どおり2行ぶんを占めていました。マスのまたぎ方(span)を変えたら、必ずスマホ側のメディアクエリでも上書きする——次章のつまずき1がこれです。CSSの書き方そのものは CSS実装テクニック集 に整理してあります。


完成条件|ここまで出せたら合格

「見本と似ている」は判定になりません。次の5つを、幅 390px・768px・1400px の3つで数字にして確かめてください。

確かめること合格の基準外れたら疑うこと
横スクロールが出ないか3つの幅すべてで scrollWidth が画面幅以下マスをまたぐ span の値と、固定pxで書いた幅
ギャラリーの段組み390pxで2列、768px以上で3列メディアクエリの境界値(768px)の書き間違い
読み込み中のズレCLSが0.1以下(枠を決めていれば実質0)枠の大きさが決まっていない画像が残っていないか
最初の画面の画像初期表示に入る lazy 画像が0枚ヒーローの img に付けた loading 属性
写真のつぶれ引き伸ばされている画像が0枚object-fit を書き忘れている img

下3つはコンソールに貼るだけで数えられます。CLSは前章のコードを使ってください。

// 横スクロールが出ていないか(false なら合格)
document.documentElement.scrollWidth > innerWidth;

// 最初の画面に lazy 画像が混ざっていないか(0 なら合格)
[...document.images].filter((i) =>
  i.loading === 'lazy' && i.getBoundingClientRect().top < innerHeight
).length;

// 引き伸ばされている画像が無いか(0 なら合格)
[...document.images].filter((i) => {
  const r = i.getBoundingClientRect();
  if (!r.height || !i.naturalWidth) return false;
  if (getComputedStyle(i).objectFit !== 'fill') return false;
  const box = r.width / r.height;
  const nat = i.naturalWidth / i.naturalHeight;
  return Math.abs(box - nat) / nat > 0.02;
}).length;

見本サイトに同じ3つを流すと390px幅で「false・0枚・0枚」、段組みは390pxで2列・768pxと1400pxで3列でした。同じ結果になれば再現としては合格です。指標の意味や実サイトでの改善の進め方は Core Web Vitals改善ガイド にあります。


つまずきポイント|実際に再現した4つ

いずれも、この記事を書く過程で実際にコードを書いて再現した症状です。憶測は入れていません。

症状原因直し方
スマホ幅で右側に余白ができ、横スクロールが出る2列のグリッドに span 3 が残っている。足りない3列目が自動で作られ、グリッドが画面より広くなる(1400px幅で確かめると横幅が2069pxまで伸びた)スマホ用のメディアクエリで span 2 に上書きする
写真が横(または縦)に伸びて見える枠と元画像の比率が違うのに object-fit が既定の fill のままimg に object-fit: cover を足す。picture 側ではなく img に書く
読み込み中に本文がガクッと下へ飛ぶ画像の高さが決まっておらず、届いた瞬間に場所を取る(実測CLS 0.4941)img に width と height を書くか、枠側に aspect-ratio を指定する
srcset を書いたのに常に大きい画像が来るsizes が無く、既定の 100vw として扱われている表示幅に合わせた sizes を書く。幅記述子(w)とセットで使う

1つ目は落とし穴として最も多い型です。PCの見た目を先に作ってからスマホを確認すると、span の値だけが取り残されます。CSSをスマホ側から書くか、幅を変えるたびに前章のスクリプトを流す習慣にしておくと早く気づけます。


前後の課題と、次に進む先

手が止まったときは、1つ前の 1つ前の上級課題(advanced001) に戻って全体の組み方を確認してください。この課題が終わったら次は 次の上級課題(advanced003) に進みます。

難しすぎると感じたら、レベルを落として組み直すほうが早いこともあります。初級から上級までの一覧は 模写コーディング課題の一覧 から選べます。


よくある質問(FAQ)

Q. 写真素材はどこで用意すればいいですか?

見本のHTMLに書かれている Unsplash の画像URLをそのまま使えます(2026-08-02に配信を確認)。幅と高さをURLのパラメータで指定できるので、srcset 用のサイズ違いを用意するのにも向いています。自分の写真に差し替えても課題の趣旨は変わりません。

Q. picture と srcset は両方書く必要がありますか?

目的が違うので必要な側だけで構いません。幅ごとにトリミングを変えたい、AVIF や WebP と従来形式を切り替えたいときは picture、同じ絵をサイズ違いで出し分けるだけなら img の srcset です。MDNも、高解像度ディスプレイ向けの出し分けだけなら picture ではなく srcset を使うよう案内しています。

Q. すべての画像に loading=”lazy” を付けてはいけませんか?

最初の画面に見えている画像には付けないでください。web.dev は「LCP画像を決して遅延読み込みしてはいけない」と明記しており、必ず読み込みの遅れを生むとしています。この課題ではヒーローの背景写真が該当します。それより下の画像には付けて構いません。

Q. width と height の属性はCSSの指定と衝突しませんか?

衝突しません。表示サイズはCSSが優先され、属性は主に縦横比の手がかりになります。CSSで height: auto にしておけば、属性から計算された比率で読み込み前の場所が確保されます。親要素の aspect-ratio で枠が決まっている場合は、そちらが場所の確保を担当します。

Q. 見本のCSSを開いて見ながら進めてもよいですか?

詰まった箇所を確認する使い方なら問題ありません。ただし見本には width・height・srcset・loading のいずれも書かれていないため、画像まわりは写しても身につきません。完成条件を先に読み、そこを自分で埋める前提で進めてください。

Q. AVIF や WebP の画像はどうやって用意しますか?

元のJPEGを変換ツールで書き出し、同じ名前で拡張子だけ変えて並べるのが手軽です。用意できなければ source を1本だけにして、img へ落ちる流れを確認するだけでも構いません。picture の要点は「上から順に見て、使えないものは飛ばし、最後の img が受け皿になる」という仕組みの理解です。

Q. どこまでできたら次の課題に進んでよいですか?

完成条件の表にある5項目がすべて基準を満たしたら合格です。余白やアニメーションの速さまで1px単位で一致させる必要はありません。幅を変えても崩れず、読み込み中に文字が飛ばないこと。この2点が上級課題の到達点です。