2026年10月8日にMDN Web PerformanceとGoogle web.dev Web Vitalsの本文を再確認した。性能は読み込み時間だけでなく、操作可能になるまで、応答性、スクロールやアニメーションの滑らかさを含む利用体験で、LCP・INP・CLSはフィールドの75パーセンタイルで見る。画像・DOM・CSS・JavaScript・ネットワークを測定し、ラボの回帰検知とRUMの実利用傾向を分け、最適化の効果を売上や検索順位へ直接換算しない。
ページが遅いとき、画像の圧縮、JavaScriptの削減、CDNの導入を同時に始めると、何が効いたか分からなくなります。最初に「表示が遅い」「操作への反応が遅い」「読んでいる位置が動く」を分け、利用者が困っている場面を一つ特定します。
この記事は、画像・JavaScript・CSS・通信・キャッシュ・サーバー・フォントを、測定から改善までつなぐ実践編です。指標と計測計画の全体はWebパフォーマンスのガイドで扱います。ここでは2026年10月4日に確認した公式資料を基に、修正する場所と変更後の確認を具体化します。
30秒で要点
- 現行のCore Web VitalsはLCP・INP・CLSです。FIDを現在の合否指標として使いません。
- 実利用のデータで対象を絞り、再現条件を固定した検証で原因を探します。Lighthouseの点数だけで利用者全体の改善は断定できません。
- LCP画像は遅延読み込みにせず、下方の画像と扱いを分けます。画像形式だけで改善率は決まりません。
- 長期キャッシュは内容が変わらない版付きURLへ。個別情報や更新されるHTMLを一律に長期保存しません。
- 改善後は性能だけでなく、表示・操作・更新反映・障害時の戻し方を確認します。
1. 何が遅いかを数値と操作で特定する
GoogleのWeb Vitalsの説明における良好な目安は次のとおりです。モバイルとデスクトップを分け、実利用データの75パーセンタイルで見ます。平均値や、自分の端末の最速値に置き換えないでください。表が収まらない場合は、横にスクロールできます
| 指標 | 見る体験 | 良好な目安 | 最初の調査先 |
|---|---|---|---|
| LCP | 主な内容が表示されるまで | 2.5秒以下 | HTML応答、主要画像、表示を妨げる処理 |
| INP | クリック・タップ・キー入力への応答 | 200ミリ秒以下 | 遅い操作、実行中の処理、次の描画までの待ち |
| CLS | 予期しない表示位置の移動 | 0.1以下 | 画像・広告・埋め込みの領域、フォント |
FIDは最初の入力の待ち時間を扱った旧指標です。INPはページ利用中の対象操作を扱うため、初期表示だけ速くしても解決しない問題があります。また、ロード中心のLighthouse検査のTBTは診断の手掛かりですが、実利用のINPそのものではありません。
実利用と検証環境を二段に分ける
PageSpeed Insightsの公式説明では、実利用のCrUXデータは過去28日間を対象にします。URL単位で不足するとオリジン単位へ切り替わる場合があり、両方不足すれば実利用データは表示できません。「データなし」は合格ではありません。- URL/オリジン、端末、対象期間、十分なデータがあるかを記録する。
- 問題のあるページと操作を選び、回線・CPU・cache・ログイン状態をそろえて再現する。
- 原因を一つ変えて同じ条件で再検証し、公開後の実利用も追う。
Lighthouse、DevTools、WebPageTestなどは原因調査に使えます。検証端末・場所・ツールの版・試行回数を残し、単発の点数差を成果率にしないでください。RUM(実利用監視)を設ける場合も、同意・送信先・収集項目・保存期間を決め、利用者の入力や秘密情報を不用意に送らない設計が必要です。
2. LCPを4つの区間へ分ける
LCPの最適化ガイドは、主に次の区間で原因を探します。表が収まらない場合は、横にスクロールできます
| 区間 | 遅くなる例 | 確かめる変更 |
|---|---|---|
| TTFB | リダイレクト、サーバー処理、通信距離 | 応答経路、DB・API時間、cacheの適用条件 |
| リソース読み込み開始まで | HTMLから画像を見つけにくい、lazy指定 | 初期HTMLで発見できるsrc/srcset、重要度 |
| リソースの読み込み | 大きな画像、遅い配信 | サイズ・画質・形式・配信経路 |
| 読み込み後の表示待ち | JavaScriptやCSSが表示を妨げる | 長い処理、表示条件、render blocking |
仮に800+600+900+700=3,000ミリ秒なら、画像の読み込み900ミリ秒だけが問題ではありません。画像転送を300ミリ秒短くしても、別の待ち時間へ移ることがあり、必ず2,700ミリ秒になるとは限りません。内訳の足し算は診断のためで、圧縮率を性能改善率に変換する式ではありません。
3. 画像は役割・寸法・形式を一緒に見直す
まず実際の表示幅より過度に大きな画像を配っていないかを調べます。JPEG・WebP・AVIFなどを、同じ内容、必要な解像度、許容できる画質で比較します。形式だけで「必ず何%小さくなる」とは言えません。透明度や細かな文字、写真の粒状感なども確認対象です。
picture要素では対応形式などの条件に応じて候補を選び、imgを代替として使います。次は幅1200・高さ675の同じ画像を3形式で用意した場合の部分HTMLです。images/hero.avif、hero.webp、hero.jpgをこの寸法・同じ構図で配置する前提です。
<picture>
<source srcset="images/hero.avif" type="image/avif">
<source srcset="images/hero.webp" type="image/webp">
<img src="images/hero.jpg" width="1200" height="675"
alt="打ち合わせで制作範囲を確認している様子"
fetchpriority="high" style="max-width:100%;height:auto">
</picture>
ここでは初期画面のLCP候補を想定し、loading="lazy"を付けていません。fetchpriority="high"も重要な画像へ限定します。すべてを高優先度にすると優先する意味が薄れます。
下方の画像はloading="lazy"を候補にし、表示領域を寸法で確保します。複数の幅のファイルを用意する場合はsrcsetと実際の表示幅に合うsizesを設定します。設定後は端末幅・画面密度・回線を変えて実際に選ばれるファイルを確認してください。HTMLの構造確認だけでは、その選択や体感速度は確認できません。
4. JavaScriptは送る量と実行中の処理を分ける
必要になるまでコードを読み込まない
初期表示に不要な画面や機能を分割します。下記はReactのJSX変換環境がある場合の2ファイルです。Details.jsxは次の内容とします。
export default function Details() {
return <p>追加の説明を表示しました。</p>;
}
App.jsxでは表示を要求したときに読み込みます。
import { lazy, Suspense, useState } from 'react';
const Details = lazy(() => import('./Details.jsx'));
export default function App() {
const [open, setOpen] = useState(false);
return (
<main>
<button type="button" onClick={() => setOpen(true)}>詳細を開く</button>
{open && <Suspense fallback={<p role="status">読み込み中</p>}><Details /></Suspense>}
</main>
);
}
Reactのlazyの仕様では、対象moduleにdefault exportが必要です。通信やmodule読込の失敗は、別途Error Boundaryなどで回復手段を用意します。この最小例は待機表示までを示し、エラー回復を実装した完成アプリではありません。
分割し過ぎると追加の待ちが増えます。tree shakingも、パッケージの形式や副作用の宣言、build設定に依存します。import文を短くしただけで削減できたとせず、production buildの出力と実際の転送を比べます。
操作の遅さを3区間で見る
INPの改善手順では、入力してから処理が始まるまで、イベント処理中、次の描画までを分けます。重い計算の分割、不要な再描画の削減、大きなDOMの見直し、必要に応じたWorkerなどを、該当する原因へ適用します。検索入力の連続処理を減らすdebounceは、「最後の入力から一定時間が過ぎたら実行する」ものです。「一定間隔に抑える」throttleとは異なります。次はdebounce.mjsとして単独でimportできる例です。
export function debounce(fn, wait = 200) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
}
debounced.cancel = () => clearTimeout(timer);
return debounced;
}
画面から離れるときなどはcancel()で待機中の呼び出しを解除します。開始済みのAPI通信まで止めるものではありません。また、200ミリ秒の待機自体が利用者の待ちになるため、確定ボタンや即時の入力反応へ機械的に使わず、検索結果の更新など目的に合う処理へ限定します。
5. CSSと表示領域を整える
CSSの削減は、未使用に見えたルールを一度の画面だけで消さないことから始めます。別route、画面幅、hover・focus、エラー、モーダル表示などで使われていないかを確認します。動的に組み立てるclassも、削除ツールの対象から漏れることがあります。
遅延読み込みとcritical rendering pathの説明では、CSSは通常、描画を待たせる資源です。初期表示に必要なCSSを小さくし、残りを分離する案はありますが、管理する出力が増えます。インライン化するならCSPとcache、重複、画面のちらつきも点検します。複雑なselectorを短くするだけで大きな効果が出るとは限りません。style計算やlayoutの時間を見てから判断します。読み取りと書き込みを交互に繰り返すDOM処理、大量要素、頻繁なサイズ変更なども原因候補です。
CLSの改善ガイドが示すように、画像、広告、埋め込みなどの場所を先に確保します。遅れて届く通知や広告を、読んでいる本文の上へ突然挿入する場合も注意が必要です。初期loadで問題がなくても、スクロールや画面遷移後に位置が動くことがあります。6. 通信・CDN・サーバーの時間を測る
通信では、リダイレクトの連鎖、接続先の数、HTMLから必要資源を発見する順序を確認します。preloadは重要な資源が発見されるのを早めるために使い、preconnectやdns-prefetchは必要な接続先へ絞ります。存在しないURLや利用しないfontを先読みしていないかも調べます。
HTTP/2やHTTP/3への対応状況は、ホスト・CDN・ブラウザで確認します。プロトコル名だけで解決とせず、実際に使われた接続と待ち時間を見ます。CDNも、利用者と配信地点、cacheのヒット、元サーバーへの往復によって効果が変わります。
サーバー側はリクエスト全体の時間を、DB、外部API、テンプレート生成などに分けます。例えば一覧の各行で同じ関連情報を取りに行く処理なら、取得回数とqueryの内容を調べ、まとめて取得できるかを検討します。インデックス追加も、対象queryと更新負荷、実行計画を確認して判断します。
圧縮はHTML・CSS・JavaScriptなどの転送量を減らす候補です。gzip/Brotliなどの対応、CPU負荷、既に圧縮済みの画像、cacheの扱いを確認します。edgeで処理する場合も、遠いDBへ往復すればその待ちが残るため、処理地点だけで速さを保証しません。
7. キャッシュは更新と個別情報の条件を付ける
HTTPキャッシュの説明では、共有cacheと個別のcacheを区別します。全拡張子へ一律に1年のcacheを付ける前に、更新時にURLを変えられるかを確認してください。表が収まらない場合は、横にスクロールできます
| 応答の種類 | 方針の例 | 先に確認すること |
|---|---|---|
| 内容hash付きの公開JS・CSS・画像 | 長いmax-ageとimmutableを候補にする | 同じURLの内容を上書きしない、旧版を参照する画面への対応 |
| 更新される公開HTML | 再検証や短い鮮度期間を設計する | ETag等、CDNの更新・失効、反映まで許される時間 |
| 個人の情報を含む応答 | 共有cacheへ保存しない。保存自体の可否も決める | private/no-store等とホスト固有設定、ログアウト後の扱い |
no-cacheは保存を全面的に禁じる名前ではなく、再利用前の再検証を要求します。no-storeは保存禁止です。どちらも、既に配布した古い応答をすべて遠隔消去できる魔法の設定ではありません。
Service Workerのcacheは別に設計します。対象URL、GET以外の扱い、個別情報の除外、版の更新、古いcacheの削除、offline時の失敗表示を決めます。画像やAPIの要求にまで共通のHTMLを返すと、型の違う応答で処理が壊れます。オフライン対応が要件でなければ、複雑さを増やす前に必要性を検討します。PWAの構成と制約も参考になります。
8. フォントは量と切り替え時の動きを確認する
フォントの実践ガイドでは、font-displayを表示方針として説明しています。swapは代替fontを表示し、到着したWeb fontへ切り替えるため、文字幅の差による位置移動が起き得ます。遅延読み込みそのものや、CLSが必ず直る設定ではありません。
まず使う書体、weight、文字範囲を棚卸しします。日本語を含む画面でLatinだけのsubsetを指定して済ませないようにします。preloadは初期表示で本当に必要なファイルに絞り、実際にCSSが参照するURLと一致させます。代替fontも含め、改行・ボタン幅・欠けが変わらないかを確認します。
ブランド表現が必須の見出しと、速く読めることを優先する本文で方針を分ける案もあります。端末の標準fontを使う選択も含め、体験と転送・保守の負担を比べます。
9. 性能予算を小さな確認から自動化する
性能予算は、自分たちで決めた上限や悪化を検知する条件です。一般的な業界標準の容量をそのまま合格点にせず、対象画面と利用条件を決めます。以下は実サイトの測定値ではなく、gzip後の初期JS容量だけを比べる仮想例です。
budget.mjsとして保存し、Node.jsでnode budget.mjsを実行できます。追加パッケージは不要です。
import assert from 'node:assert/strict';
function withinBudget(bytes, limit) {
if (!Number.isSafeInteger(bytes) || bytes < 0 ||
!Number.isSafeInteger(limit) || limit < 0) {
throw new TypeError('bytes and limit must be non-negative safe integers');
}
return bytes <= limit;
}
const before = 300 * 1024;
const after = 240 * 1024;
const limit = 250 * 1024;
assert.equal(withinBudget(before, limit), false);
assert.equal(withinBudget(after, limit), true);
assert.equal(withinBudget(limit, limit), true);
assert.throws(() => withinBudget(-1, limit), TypeError);
assert.throws(() => withinBudget(NaN, limit), TypeError);
console.log({ savedBytes: before - after, reductionPercent: (before - after) / before * 100 });
ここでは300KiBから240KiBへ60KiB、61,440bytes、20%減ります。仮の上限250KiB以内になりましたが、INPや売上が20%改善したという意味ではありません。実運用では、同じ圧縮条件で計測した出力を入力にし、計測失敗をゼロとして通さない処理も必要です。
CIへ組み込む場合は、production build、対象URL、ツール版、端末・回線条件、試行回数、失敗時の扱いを固定します。容量の検査、Lighthouseなどの検証環境の測定、公開後の実利用監視は別のチェックとして記録します。CI/CDの基礎に沿って、警告だけか公開を止めるかを担当者と決めます。
10. 改善を確定する前の確認
変更ごとに、対象の症状、原因の仮説、変更点、前後の条件、観測値、機能への影響、戻し方を1枚に残します。直す順番は「利用者への影響」と「原因の確からしさ」で決めます。
- 画像変更後は画質・代替テキスト・寸法・端末ごとの選択を確認する。
- JSやCSS削減後は、入力・エラー・フォーカス・戻る操作・別画面を確認する。
- cache変更後は、初回・再訪・更新直後・ログイン/ログアウト・古いタブを確認する。
- server変更後は、失敗時・ピーク時・DBや外部APIの遅延と復旧を確認する。
本稿のコードはソースと記事変換後の文字列を照合し、算数・Nodeの実行・HTML構造などをローカルで確認する範囲です。実ブラウザでの表示、通信、性能測定、本番のcacheやサービスは別途検証が必要です。架空の改善率を実績として報告せず、確認できた条件と未確認の範囲を一緒に残してください。