- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 1. なぜパフォーマンスが重要(じゅうよう)なのか
- 2. Core Web Vitals深掘(ふかぼ)り分析(ぶんせき)
- 3. 画像(がぞう)最適化(さいてきか)
- 4. JavaScript最適化(さいてきか)
- 5. CSS最適化(さいてきか)
- 6. フォント最適化(さいてきか)
- 7. レンダリングパターン
- 8. キャッシュ戦略(せんりゃく)
- 9. ネットワーク最適化(さいてきか)
- 10. パフォーマンスモニタリング
- 11. Next.js特化(とっか)最適化(さいてきか)
- 12. 面接(めんせつ)質問集(しつもんしゅう)
- 13. クイズ
- 14. 参考(さんこう)資料(しりょう)
はじめに
Webパフォーマンスはユーザー体験(たいけん)とビジネス成果(せいか)に直接的(ちょくせつてき)な影響(えいきょう)を与(あた)えます。Googleの研究(けんきゅう)によると、ページロード時間(じかん)が1秒(びょう)から3秒(びょう)に増(ふ)えると離脱率(りだつりつ)が32%増加(ぞうか)し、5秒(びょう)になると90%増加(ぞうか)します。Amazonはページロード時間(じかん)が100ms増(ふ)えるたびに売上(うりあげ)が1%減少(げんしょう)すると報告(ほうこく)しています。
2025年(ねん)、GoogleはCore Web Vitalsを検索(けんさく)ランキング要素(ようそ)として強化(きょうか)し、INP(Interaction to Next Paint)がFIDを置(お)き換(か)えたことで、インタラクション性能(せいのう)の重要性(じゅうようせい)がさらに高(たか)まりました。この記事(きじ)では、Core Web Vitalsの最適化(さいてきか)から画像(がぞう)、JavaScript、CSS、フォント、レンダリングパターン、キャッシュ、ネットワーク、モニタリング、そしてNext.js特化(とっか)の最適化(さいてきか)まで、Webパフォーマンスの全(すべ)てをカバーします。
1. なぜパフォーマンスが重要(じゅうよう)なのか
1.1 ビジネスインパクト
パフォーマンスとビジネス指標:
├── ロード3秒超過 → 53%離脱 (Google)
├── 100ms遅延 → 売上1%減少 (Amazon)
├── 500ms遅延 → トラフィック20%減少 (Google)
├── 1秒改善 → コンバージョン率7%向上 (Walmart)
└── 2秒改善 → バウンス率50%減少 (COOK)
1.2 SEOとCore Web Vitals
2025年(ねん)、GoogleはCore Web Vitalsを検索(けんさく)ランキングの核心(かくしん)要素(ようそ)として確定(かくてい)しました。
| 指標(しひょう) | 良好(りょうこう)(Good) | 改善必要(かいぜんひつよう) | 不良(ふりょう)(Poor) |
|---|---|---|---|
| LCP | 2.5秒(びょう)以内(いない) | 2.5〜4.0秒 | 4.0秒超過(ちょうか) |
| INP | 200ms以内 | 200〜500ms | 500ms超過 |
| CLS | 0.1以下(いか) | 0.1〜0.25 | 0.25超過 |
1.3 パフォーマンスバジェット
パフォーマンスバジェット例:
├── 初期ロードJS: 200KB以下 (gzip)
├── 初期ロードCSS: 50KB以下 (gzip)
├── 全ページサイズ: 1.5MB以下
├── LCP: 2.5秒以内
├── INP: 200ms以内
├── CLS: 0.1以下
├── Time to First Byte: 600ms以内
└── リクエスト数: 50個以下
2. Core Web Vitals深掘(ふかぼ)り分析(ぶんせき)
2.1 LCP(Largest Contentful Paint)
LCPはビューポート内(ない)で最(もっと)も大(おお)きなコンテンツ要素(ようそ)がレンダリングされる時間(じかん)です。
LCP対象要素:
├── img要素
├── video要素(ポスター画像)
├── CSS background-imageを持つ要素
├── テキストノードを含むブロックレベル要素
└── svg内のimage要素
LCP最適化(さいてきか)戦略(せんりゃく):
<!-- 1. ヒーロー画像のプリロード -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- 2. LCP画像にfetchpriority設定 -->
<img src="/hero.webp" alt="Hero" fetchpriority="high" loading="eager" />
<!-- 3. サーバー応答時間の最適化 -->
<!-- TTFB目標: 200ms以内 -->
/* 4. フォントローディング最適化 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap;
}
2.2 INP(Interaction to Next Paint)
INPはユーザーインタラクション(クリック、タップ、キーボード)から次(つぎ)のペイントまでの遅延(ちえん)時間(じかん)を測定(そくてい)します。
INP最適化戦略:
1. 長いタスクの分割
- 50ms以上のタスクをより小さな単位に分割
- requestIdleCallback、scheduler.yield()活用
2. メインスレッドの解放
- Web Workerに重い計算を移動
- 不要な同期JSの除去
3. イベントハンドラーの最適化
- debounce / throttle適用
- passiveイベントリスナー使用
// 長いタスク分割の例
async function processLargeList(items: Item[]) {
const CHUNK_SIZE = 50;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
processChunk(chunk);
// ブラウザにレンダリングの機会を提供
await scheduler.yield();
}
}
// scheduler.yieldポリフィル
if (!('scheduler' in globalThis)) {
(globalThis as any).scheduler = {
yield: () => new Promise(resolve => setTimeout(resolve, 0))
};
}
2.3 CLS(Cumulative Layout Shift)
CLSはページロード中(ちゅう)の予期(よき)しないレイアウトシフトの合計(ごうけい)です。
<!-- CLS防止: 画像にサイズを明示 -->
<img src="/photo.webp" width="800" height="600" alt="Photo" />
<!-- CLS防止: 動的コンテンツにmin-heightを設定 -->
<div style="min-height: 250px;">
<!-- 広告や動的コンテンツ -->
</div>
/* CLS防止: フォントローディング時のレイアウトシフト最小化 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional; /* またはswap */
size-adjust: 100.5%;
ascent-override: 95%;
descent-override: 22%;
line-gap-override: 0%;
}
CLSの主要(しゅよう)原因(げんいん)と解決策(かいけつさく):
| 原因(げんいん) | 解決策(かいけつさく) |
|---|---|
| サイズ未指定(みしてい)の画像 | width/heightまたはaspect-ratio属性使用 |
| 動的挿入(そうにゅう)コンテンツ | min-heightでスペース予約(よやく) |
| Webフォント FOUT/FOIT | font-display: optional + preload |
| 動的広告(こうこく) | 固定(こてい)サイズコンテナ使用 |
| 遅(おく)れてロードされるCSS | Critical CSSをインライン化 |
3. 画像(がぞう)最適化(さいてきか)
3.1 次世代(じせだい)フォーマット
画像フォーマット比較(同品質基準):
├── JPEG: 100KB (基準)
├── WebP: 70KB (-30%)
├── AVIF: 50KB (-50%)
└── JXL (JPEG XL): 55KB (-45%)
ブラウザサポート (2025):
├── WebP: 97%+ (IE非対応)
├── AVIF: 92%+ (Safari 16.4+)
└── JXL: Chromeで削除、Safari/Firefoxサポート
<!-- picture要素でフォーマットフォールバック -->
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>
3.2 レスポンシブ画像(がぞう)
<!-- srcsetとsizesでレスポンシブ画像 -->
<img
srcset="
/hero-400w.webp 400w,
/hero-800w.webp 800w,
/hero-1200w.webp 1200w,
/hero-1600w.webp 1600w
"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
src="/hero-800w.webp"
alt="Hero image"
width="1200"
height="600"
loading="lazy"
decoding="async"
/>
3.3 遅延(ちえん)ロードとブラープレースホルダー
<!-- ネイティブlazy loading -->
<img src="/photo.webp" loading="lazy" decoding="async" alt="Photo" />
// ブラープレースホルダーの実装
function BlurImage({ src, alt, width, height, blurDataURL }: ImageProps) {
return (
<div style={{ position: 'relative', width, height }}>
{/* ブラープレースホルダー */}
<img
src={blurDataURL}
alt=""
style={{
position: 'absolute',
inset: 0,
filter: 'blur(20px)',
transform: 'scale(1.1)',
}}
/>
{/* 実際の画像 */}
<img
src={src}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
onLoad={(e) => {
e.currentTarget.style.opacity = '1';
}}
style={{ position: 'relative', opacity: 0, transition: 'opacity 0.3s' }}
/>
</div>
);
}
4. JavaScript最適化(さいてきか)
4.1 コード分割(ぶんかつ)(Code Splitting)
// React lazy + Suspense(ルートベース分割)
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// 条件付き動的import
async function handleExport() {
// xlsxライブラリを必要な時だけロード
const XLSX = await import('xlsx');
const workbook = XLSX.utils.book_new();
// ...
}
4.2 Tree Shaking
// 悪い例: ライブラリ全体をimport
import _ from 'lodash';
_.debounce(fn, 300);
// 良い例: 必要な関数のみimport
import debounce from 'lodash/debounce';
debounce(fn, 300);
// さらに良い例: ネイティブまたは軽量な代替を使用
// lodash.debounce: 1.4KB vs lodash: 72KB
// webpack-bundle-analyzerでバンドル分析
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Next.js config
});
4.3 バンドル最適化(さいてきか)
// webpack splitChunks設定
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
common: {
minChunks: 2,
priority: -10,
reuseExistingChunk: true,
},
},
},
},
};
4.4 スクリプトローディング戦略(せんりゃく)
<!-- 通常: パース をブロック -->
<script src="app.js"></script>
<!-- async: 非同期ダウンロード、即時実行(パースブロック可能) -->
<script src="analytics.js" async></script>
<!-- defer: 非同期ダウンロード、DOMパース後に実行(順序保証) -->
<script src="app.js" defer></script>
<!-- module: deferと同じ動作 -->
<script type="module" src="app.js"></script>
スクリプトローディングタイムライン:
通常: [HTMLパース...] [ダウンロード] [実行] [HTMLパース...]
async: [HTMLパース......ダウンロード......] [実行] [HTMLパース...]
defer: [HTMLパース......ダウンロード.............] [実行]
5. CSS最適化(さいてきか)
5.1 Critical CSS
<!-- Critical CSSをインライン化 -->
<head>
<style>
/* ファーストビューに必要な最小限のCSSのみインライン */
body { margin: 0; font-family: system-ui; }
.header { height: 60px; background: #fff; }
.hero { height: 400px; display: flex; align-items: center; }
</style>
<!-- 残りのCSSは非同期ロード -->
<link rel="preload" href="/styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>
5.2 CSS Containment
/* containでレンダリング範囲を制限 */
.card {
contain: layout style paint;
/* またはcontent-visibilityでオフスクリーン要素を最適化 */
content-visibility: auto;
contain-intrinsic-size: 0 300px;
}
/* will-changeでGPUアクセラレーションヒント */
.animated-element {
will-change: transform;
/* 注意: 乱用するとメモリ浪費 */
}
5.3 未使用(みしよう)CSSの除去(じょきょ)
// PurgeCSS設定 (postcss.config.js)
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./public/index.html'
],
defaultExtractor: content =>
content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: ['html', 'body', /^data-/]
})
]
};
6. フォント最適化(さいてきか)
6.1 font-display戦略(せんりゃく)
/* swap: 代替フォント → Webフォント(FOUT発生、テキスト即時表示) */
@font-face {
font-family: 'MyFont';
font-display: swap;
src: url('/fonts/myfont.woff2') format('woff2');
}
/* optional: 速ければWebフォント、遅ければ代替フォント維持(CLS最小) */
@font-face {
font-family: 'MyFont';
font-display: optional;
src: url('/fonts/myfont.woff2') format('woff2');
}
6.2 フォントプリロード
<!-- コアフォントのプリロード -->
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>
6.3 Variable Fonts
/* 従来: 各ウェイト別に別ファイル(400, 500, 600, 700 = 4ファイル) */
/* Variable Font: 1ファイルで全ウェイト */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
/* 使用 */
.light { font-weight: 300; }
.regular { font-weight: 400; }
.bold { font-weight: 700; }
6.4 フォントサブセット
# pyftsubsetで日本語フォントサブセット生成
# pip install fonttools brotli
pyftsubset NotoSansJP-Regular.otf \
--text-file=japanese-chars.txt \
--output-file=NotoSansJP-subset.woff2 \
--flavor=woff2
# 結果: 4.5MB → 500KB(常用漢字+ひらがな+カタカナ基準)
7. レンダリングパターン
7.1 パターン比較表(ひかくひょう)
| パターン | TTFB | FCP | LCP | TTI | SEO | 使用(しよう)場面(ばめん) |
|---|---|---|---|---|---|---|
| CSR | 速(はや)い | 遅(おそ)い | 遅い | 遅い | 悪(わる)い | SPA、ダッシュボード |
| SSR | 遅い | 速い | 速い | 遅い | 良(よ)い | 動的(どうてき)コンテンツ、SEO必要 |
| SSG | 非常(ひじょう)に速い | 非常に速い | 非常に速い | 速い | 非常に良い | ブログ、ドキュメント |
| ISR | 非常に速い | 非常に速い | 非常に速い | 速い | 非常に良い | Eコマース、ニュース |
| Streaming SSR | 速い | 非常に速い | 速い | 速い | 良い | 複雑(ふくざつ)な動的ページ |
7.2 CSR(Client-Side Rendering)
CSRフロー:
Browser Server
│─ HTML要求 ────→│
│←─ 空のHTML ────│
│─ JS要求 ──────→│
│←─ JSバンドル ──│
│ [パース/実行] │
│─ API要求 ─────→│
│←─ データ ──────│
│ [レンダリング] │
▼ 画面表示 ▼
7.3 SSR(Server-Side Rendering)
SSRフロー:
Browser Server
│─ HTML要求 ────→│
│ │ [データフェッチ]
│ │ [HTMLレンダリング]
│←─ 完成HTML ───│
│ [画面表示] │
│─ JS要求 ──────→│
│←─ JSバンドル ──│
│ [Hydration] │
▼ インタラクティブ ▼
7.4 Streaming SSR
// Next.js App Router Streaming SSR
import { Suspense } from 'react';
async function SlowComponent() {
const data = await fetchSlowData(); // 3秒かかる
return <div>{/* データレンダリング */}</div>;
}
export default function Page() {
return (
<div>
{/* 即時レンダリング */}
<Header />
<Hero />
{/* ストリーミング: 準備できたら段階的に送信 */}
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
{/* 即時レンダリング */}
<Footer />
</div>
);
}
7.5 ISR(Incremental Static Regeneration)
// Next.js ISR
// app/products/[id]/page.tsx
export const revalidate = 3600; // 1時間ごとに再生成
async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
export default ProductPage;
// 静的パス生成
export async function generateStaticParams() {
const products = await getTopProducts(100);
return products.map(p => ({ id: p.id }));
}
8. キャッシュ戦略(せんりゃく)
8.1 HTTPキャッシュ
HTTPキャッシュ戦略:
├── 静的アセット(JS/CSS/画像)
│ Cache-Control: public, max-age=31536000, immutable
│ (ファイル名にハッシュ含む: app.abc123.js)
│
├── HTML
│ Cache-Control: public, max-age=0, must-revalidate
│ (常にサーバー確認)
│
├── APIレスポンス
│ Cache-Control: private, max-age=60, stale-while-revalidate=300
│ (60秒キャッシュ、5分間stale許容)
│
└── ユーザー固有データ
Cache-Control: private, no-cache
(毎回サーバー検証)
8.2 stale-while-revalidate
stale-while-revalidateの動作:
リクエスト1: [キャッシュミス] → サーバー → レスポンス + キャッシュ保存
リクエスト2: [キャッシュヒット, fresh] → 即時レスポンス
リクエスト3: [キャッシュヒット, stale] → 即時レスポンス(stale) + バックグラウンド更新
リクエスト4: [キャッシュヒット, fresh] → 即時レスポンス(更新済みデータ)
// SWRライブラリ(React)
import useSWR from 'swr';
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 30000, // 30秒ごとに更新
});
if (isLoading) return <Skeleton />;
if (error) return <Error />;
return <UserCard user={data} />;
}
8.3 Service Workerキャッシュ
// Service Workerキャッシング戦略
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/styles.css',
'/app.js',
'/offline.html'
];
// インストール: 静的アセットをプリキャッシュ
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache =>
cache.addAll(STATIC_ASSETS)
)
);
});
// フェッチ: Cache First + Network Fallback
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return fetch(event.request).then(response => {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache =>
cache.put(event.request, clone)
);
return response;
}).catch(() => {
return caches.match('/offline.html');
});
})
);
});
8.4 CDNキャッシュ
CDNキャッシュ階層:
ユーザー → [ブラウザキャッシュ] → [CDN Edge] → [CDN Origin Shield] → [サーバー]
CDNヘッダー例:
静的アセット:
Cache-Control: public, max-age=31536000, immutable
CDN-Cache-Control: public, max-age=31536000
動的コンテンツ:
Cache-Control: public, max-age=0, must-revalidate
CDN-Cache-Control: public, max-age=60, stale-while-revalidate=300
Surrogate-Control: max-age=3600
9. ネットワーク最適化(さいてきか)
9.1 リソースヒント
<!-- DNS Prefetch: 外部ドメインのDNSを先行解決 -->
<link rel="dns-prefetch" href="//api.example.com" />
<link rel="dns-prefetch" href="//cdn.example.com" />
<!-- Preconnect: DNS + TCP + TLSを先行接続 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Prefetch: 次ページのリソースを先行ロード -->
<link rel="prefetch" href="/next-page.js" />
<!-- Preload: 現ページの重要リソースを優先ロード -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/hero.webp" as="image" />
9.2 Priority Hints
<!-- fetchpriorityでリソース優先順位を指定 -->
<!-- ヒーロー画像: 高優先度 -->
<img src="/hero.webp" fetchpriority="high" />
<!-- オフスクリーン画像: 低優先度 -->
<img src="/below-fold.webp" fetchpriority="low" loading="lazy" />
<!-- 重要スクリプト: 高優先度 -->
<script src="/critical.js" fetchpriority="high"></script>
9.3 HTTP/2とHTTP/3
HTTP/1.1 vs HTTP/2 vs HTTP/3:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 機能 │ HTTP/1.1 │ HTTP/2 │ HTTP/3 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 多重化 │ 不可 │ 対応 │ 対応 │
│ ヘッダー圧縮 │ なし │ HPACK │ QPACK │
│ サーバープッシュ│ なし │ 対応 │ 廃止 │
│ HOLブロッキング│ TCPレベル │ TCPレベル │ 解決(QUIC) │
│ 転送プロトコル │ TCP │ TCP │ QUIC(UDP) │
│ 接続確立 │ TCP+TLS │ TCP+TLS │ 0-RTT可能 │
└──────────────┴──────────────┴──────────────┴──────────────┘
10. パフォーマンスモニタリング
10.1 Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
configPath: './lighthouserc.json'
uploadArtifacts: true
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.9 }],
"categories:best-practices": ["warn", { "minScore": 0.9 }],
"categories:seo": ["error", { "minScore": 0.9 }]
}
}
}
}
10.2 Real User Monitoring(RUM)
// Web Vitalsの測定と報告
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: any) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// Beacon APIで非同期送信
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', body);
} else {
fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
10.3 パフォーマンスダッシュボード
パフォーマンスモニタリングツール:
├── 合成モニタリング(Lab Data)
│ ├── Lighthouse CI(自動化)
│ ├── WebPageTest(詳細分析)
│ └── PageSpeed Insights(Google)
│
├── 実ユーザーモニタリング(Field Data)
│ ├── Chrome UX Report(CrUX)
│ ├── web-vitalsライブラリ
│ └── 商用: Datadog RUM、New Relic
│
└── バンドル分析
├── webpack-bundle-analyzer
├── source-map-explorer
└── bundlephobia.com
11. Next.js特化(とっか)最適化(さいてきか)
11.1 App RouterとServer Components
// Server Component(デフォルト - JSバンドルに含まれない)
// app/products/page.tsx
async function ProductsPage() {
// サーバーで直接データフェッチ(APIルート不要)
const products = await db.product.findMany();
return (
<div>
<h1>Products</h1>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
// Client Component(インタラクションが必要な部分のみ)
// components/AddToCart.tsx
'use client';
import { useState } from 'react';
export function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Add to Cart ({count})
</button>
);
}
11.2 Next.js Image最適化(さいてきか)
import Image from 'next/image';
// 自動WebP/AVIF変換、レスポンシブ、lazy loading
function HeroSection() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // LCP画像にはpriority設定
sizes="(max-width: 768px) 100vw, 1200px"
quality={85}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// リモート画像
function Avatar({ user }: { user: User }) {
return (
<Image
src={user.avatarUrl}
alt={user.name}
width={48}
height={48}
loading="lazy"
/>
);
}
11.3 Next.js Font最適化(さいてきか)
// next/fontで自動最適化(セルフホスティング、CLS除去)
import { Inter, Noto_Sans_JP } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const notoSansJP = Noto_Sans_JP({
subsets: ['latin'],
weight: ['400', '500', '700'],
display: 'swap',
variable: '--font-noto-sans-jp',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja" className={`${inter.variable} ${notoSansJP.variable}`}>
<body>{children}</body>
</html>
);
}
11.4 Route Segment Config
// app/blog/[slug]/page.tsx
// 静的生成 + ISR(60秒)
export const revalidate = 60;
// または完全に静的
export const dynamic = 'force-static';
// または常に動的
export const dynamic = 'force-dynamic';
// ランタイム選択
export const runtime = 'edge'; // Edge Runtime(速いTTFB)
// export const runtime = 'nodejs'; // Node.jsランタイム(デフォルト)
12. 面接(めんせつ)質問集(しつもんしゅう)
基本概念(きほんがいねん)
Q1. Core Web Vitals 3つの指標(しひょう)を説明(せつめい)してください。
-
LCP(Largest Contentful Paint): ビューポートで最(もっと)も大(おお)きなコンテンツ要素(ようそ)がレンダリングされる時間(じかん)。目標(もくひょう): 2.5秒以内(いない)。ローディング性能(せいのう)を測定(そくてい)。
-
INP(Interaction to Next Paint): ユーザーインタラクション(クリック、キーボードなど)から次(つぎ)のペイントまでの遅延(ちえん)。目標: 200ms以内。応答性(おうとうせい)を測定。
-
CLS(Cumulative Layout Shift): ページロード中(ちゅう)の予期(よき)しないレイアウトシフトの合計(ごうけい)。目標: 0.1以下(いか)。視覚的(しかくてき)安定性(あんていせい)を測定。
Q2. CSR、SSR、SSG、ISRの違(ちが)いを説明(せつめい)してください。
CSR(Client-Side Rendering): ブラウザでJSを使(つか)ってレンダリング。空(から)のHTMLを送信(そうしん)後(ご)、クライアントでレンダリング。
- 長所(ちょうしょ): サーバー負荷(ふか)低(ひく)い、SPA向(む)き
- 短所(たんしょ): 初期(しょき)ロードが遅(おそ)い、SEO不利(ふり)
SSR(Server-Side Rendering): サーバーがリクエストごとにHTMLを生成(せいせい)。
- 長所: 速(はや)いFCP、SEO有利(ゆうり)
- 短所: サーバー負荷、遅いTTFB
SSG(Static Site Generation): ビルド時(じ)にHTMLを事前(じぜん)生成。
- 長所: 最高(さいこう)のパフォーマンス、CDNキャッシュ
- 短所: ビルド時間、動的(どうてき)コンテンツの制限(せいげん)
ISR(Incremental Static Regeneration): SSG + バックグラウンド再生成(さいせいせい)。
- 長所: SSGのパフォーマンス + データ更新(こうしん)
- 短所: 実装(じっそう)の複雑さ(ふくざつさ)
Q3. Code SplittingとTree Shakingの違(ちが)いを説明(せつめい)してください。
Code Splitting: JSバンドルを複数(ふくすう)のチャンクに分割(ぶんかつ)し、必要(ひつよう)なものだけをロードする技法(ぎほう)。ルートベース(dynamic import)またはコンポーネントベースで分割。初期(しょき)ロード時間(じかん)の短縮(たんしゅく)。
Tree Shaking: ビルド時(じ)に使用(しよう)されていない(dead)コードを除去(じょきょ)する最適化(さいてきか)。ES Moduleの静的(せいてき)構造(こうぞう)を分析(ぶんせき)して、importされていないexportを除去。最終(さいしゅう)バンドルサイズの削減(さくげん)。
違い: Code Splittingは「いつロードするか」(When)、Tree Shakingは「何(なに)を除去するか」(What)。
Q4. LCPを改善(かいぜん)する方法(ほうほう)を説明(せつめい)してください。
- サーバー応答最適化(さいてきか): TTFB 200ms以内(いない)、CDN活用(かつよう)、キャッシュ
- リソースプリロード: LCP画像(がぞう)にpreload + fetchpriority="high"
- 画像最適化: WebP/AVIF、適切(てきせつ)なサイズ、圧縮(あっしゅく)
- レンダリングブロック除去(じょきょ): Critical CSSインライン、JS defer
- フォント最適化: font-display: swap、preload
- SSR/SSG活用: サーバーから完成(かんせい)HTMLを送信
- サードパーティ最適化: 外部(がいぶ)スクリプトの遅延(ちえん)ロード
Q5. CLSの主要(しゅよう)な原因(げんいん)と解決方法(かいけつほうほう)を説明してください。
主要な原因:
- サイズ未指定(みしてい)の画像(がぞう)/動画(どうが)
- 動的挿入(どうてきそうにゅう)コンテンツ(広告(こうこく)、バナー)
- Webフォントロード時(じ)のレイアウトシフト
- 遅(おく)れてロードされるCSS
- DOMを操作(そうさ)するJavaScript
解決方法:
- img/videoにwidth、height属性(ぞくせい)またはaspect-ratio CSSを明示(めいじ)
- 動的コンテンツ領域(りょういき)にmin-heightでスペース予約(よやく)
- font-display: optional、フォントプリロード
- Critical CSSインライン化
- transformアニメーション使用(top/leftの代(か)わりに)
深掘(ふかぼ)り質問(しつもん)
Q6. Service Workerのキャッシング戦略(せんりゃく)を説明(せつめい)してください。
- Cache First: キャッシュ確認(かくにん)後(ご)、なければネットワーク。静的(せいてき)アセットに最適(さいてき)。
- Network First: ネットワーク試行(しこう)後(ご)、失敗(しっぱい)時(じ)にキャッシュ。APIレスポンスに最適。
- Stale While Revalidate: キャッシュされたレスポンスを即時(そくじ)返(かえ)す + バックグラウンドで更新(こうしん)。頻繁(ひんぱん)に変更(へんこう)されるアセットに最適。
- Cache Only: キャッシュのみ使用。オフラインアセットに最適。
- Network Only: ネットワークのみ使用。リアルタイムデータに最適。
選択基準(せんたくきじゅん): データの鮮度(せんど)要件(ようけん)とオフラインサポートの必要性(ひつようせい)に応(おう)じて決定(けってい)。
Q7. HTTPキャッシュヘッダーを説明(せつめい)してください。
-
Cache-Control: キャッシュポリシーの核心(かくしん)ヘッダー
- max-age: キャッシュ有効期間(ゆうこうきかん)(秒(びょう))
- no-cache: 毎回(まいかい)サーバー検証(けんしょう)必要
- no-store: 絶対(ぜったい)にキャッシュしない
- public/private: CDNキャッシュ可否(かひ)
- immutable: 変(か)わらないリソース
- stale-while-revalidate: staleレスポンス許容(きょよう)時間
-
ETag: リソースバージョン識別子(しきべつし)。条件付(じょうけんつ)きリクエスト(If-None-Match)に使用。
-
Last-Modified: 最終(さいしゅう)更新(こうしん)時間。If-Modified-Sinceと併用(へいよう)。
推奨戦略(すいしょうせんりゃく):
- 静的アセット(ハッシュ含(ふく)む): max-age=31536000, immutable
- HTML: no-cacheまたはmax-age=0, must-revalidate
Q8. 画像(がぞう)最適化(さいてきか)戦略を総合的(そうごうてき)に説明(せつめい)してください。
- フォーマット: WebP/AVIF使用(しよう)、picture要素(ようそ)でフォールバック
- サイズ: レスポンシブ画像(srcset + sizes)、実際(じっさい)の表示(ひょうじ)サイズに合(あ)わせる
- 圧縮(あっしゅく): 品質75〜85、用途(ようと)に応(おう)じて調整(ちょうせい)
- ローディング: LCP画像はeager + fetchpriority="high"、残(のこ)りはlazy
- フレームワーク: Next.js Imageなど自動最適化ツール活用(かつよう)
- CDN: 画像CDN(Cloudinary、imgix)活用
- プレースホルダー: ブラーまたはLQIPでUX向上(こうじょう)
Q9. INP(Interaction to Next Paint)を最適化(さいてきか)する方法(ほうほう)を説明してください。
- 長(なが)いタスクの分割(ぶんかつ): 50ms以上(いじょう)の作業(さぎょう)をscheduler.yield()やrequestIdleCallbackで分割
- メインスレッドの解放(かいほう): Web Workerに重(おも)い計算(けいさん)を移動(いどう)
- イベントハンドラー最適化: debounce/throttle、passiveイベントリスナー
- JSバンドル最小化(さいしょうか): コードスプリッティング、ツリーシェイキング
- 仮想化(かそうか): 長(なが)いリストはreact-virtuosoなどで仮想化
- startTransition: 緊急(きんきゅう)でない更新(こうしん)をトランジションとしてマーク
- React最適化: useMemo、useCallback、React.memoを適切(てきせつ)に使用
Q10. CDNの動作原理(どうさげんり)と性能上(せいのうじょう)の利点(りてん)を説明(せつめい)してください。
動作原理:
- 世界中(せかいじゅう)に分散(ぶんさん)されたEdgeサーバー(PoP)にコンテンツをキャッシュ
- ユーザーのDNSリクエストを最(もっと)も近(ちか)いEdgeにルーティング
- Edgeにキャッシュがあれば即時(そくじ)応答(おうとう)、なければOriginから取得(しゅとく)してキャッシュ
パフォーマンスの利点(りてん):
- 物理的(ぶつりてき)距離(きょり)の短縮(たんしゅく): RTT(Round Trip Time)の削減(さくげん)
- Originサーバーの負荷(ふか)分散(ぶんさん)
- DDoS防御(ぼうぎょ)
- TLS最適化(EdgeでTLS終端(しゅうたん))
- 自動圧縮(じどうあっしゅく)(Brotli、gzip)
- HTTP/2、HTTP/3サポート
Q11. webpackのコードスプリッティング設定(せってい)を説明(せつめい)してください。
webpackのsplitChunksプラグインでコード分割(ぶんかつ):
- Entry Points: 複数(ふくすう)のエントリーポイントで手動(しゅどう)分割
- Dynamic Imports: import()で動的(どうてき)分割
- splitChunks: 自動(じどう)分割ルール設定(せってい)
主要(しゅよう)な設定:
- chunks: 'all'で同期(どうき)/非同期(ひどうき)の両方(りょうほう)を分割
- cacheGroups: vendor(node_modules)とcommon(共有(きょうゆう)モジュール)を分離(ぶんり)
- minSize: 最小(さいしょう)チャンクサイズ(デフォルト20KB)
- maxSize: 最大(さいだい)チャンクサイズ(自動分割)
効果(こうか): 初期ロード時間短縮(たんしゅく)、キャッシュ効率(こうりつ)向上(こうじょう)、必要(ひつよう)なコードのみロード
Q12. Streaming SSRの利点(りてん)と実装方法(じっそうほうほう)を説明(せつめい)してください。
利点:
- TTFB短縮(たんしゅく): HTMLを段階的(だんかいてき)に送信(そうしん)
- FCP向上(こうじょう): 準備(じゅんび)できた部分(ぶぶん)から表示(ひょうじ)
- 遅(おそ)いデータソースの隔離(かくり): Suspenseで囲(かこ)んで独立(どくりつ)ロード
- UX向上: スケルトン/ローディング状態(じょうたい)を即時(そくじ)表示
実装(Next.js App Router):
- Server Componentをデフォルトで使用
- 遅いコンポーネントをSuspenseで囲む
- fallbackにスケルトンUIを提供(ていきょう)
- データが準備(じゅんび)できたら自動的(じどうてき)にストリーミング
核心(かくしん): ページ全体(ぜんたい)が準備されるまで待(ま)たず、準備できた部分から段階的(だんかいてき)に送信(そうしん)。
Q13. Next.js Server Componentsのパフォーマンス上(じょう)の利点(りてん)を説明(せつめい)してください。
- ゼロJSバンドル: Server ComponentsはクライアントJSバンドルに含(ふく)まれない
- 直接(ちょくせつ)データアクセス: サーバーで直接DB/APIにアクセス(APIルート不要(ふよう))
- 自動(じどう)コード分割(ぶんかつ): Client Componentsのみバンドルに含まれる
- ストリーミング: Suspenseと組(く)み合(あ)わせて段階的(だんかいてき)レンダリング
- キャッシュ: サーバー側(がわ)キャッシュで繰(く)り返(かえ)しリクエストを最適化(さいてきか)
- セキュリティ: 機密(きみつ)ロジック/キーがクライアントに露出(ろしゅつ)しない
使用原則(しようげんそく): インタラクションのないコンポーネントはServer、useState/useEffectが必要(ひつよう)な部分(ぶぶん)のみClient。
Q14. パフォーマンスバジェットの設定(せってい)と適用方法(てきようほうほう)は?
設定:
- 競合分析(きょうごうぶんせき): 主要(しゅよう)な競合(きょうごう)のパフォーマンス指標(しひょう)を測定(そくてい)
- ユーザーデバイス分析: ターゲットユーザーの平均(へいきん)デバイス/ネットワークを把握(はあく)
- ビジネス目標(もくひょう)の反映(はんえい): コンバージョン率、離脱率(りだつりつ)目標に合(あ)わせて設定
- 具体的(ぐたいてき)な数値(すうち): JS 200KB、CSS 50KB、LCP 2.5s、INP 200msなど
適用:
- Lighthouse CIでPRごとに自動検査(じどうけんさ)
- バジェット超過時(ちょうかじ)にビルド失敗(しっぱい)または警告(けいこく)
- bundlesizeまたはsize-limitでバンドルサイズ制限(せいげん)
- チームダッシュボードでトレンドモニタリング
- 定期的(ていきてき)にバジェットのレビューと調整(ちょうせい)
Q15. Webフォント最適化(さいてきか)のベストプラクティスを説明(せつめい)してください。
- Variable Font: 1つのファイルで全(すべ)てのウェイト/スタイル(ファイル数(すう)削減(さくげん))
- サブセット: 必要(ひつよう)な文字(もじ)のみ含(ふく)む(日本語(にほんご): 4MB → 500KB)
- WOFF2フォーマット: 最高(さいこう)圧縮率(あっしゅくりつ)のWebフォントフォーマット
- font-display: swap(テキスト即時(そくじ)表示(ひょうじ))またはoptional(CLS最小(さいしょう))
- preload: コアフォントをlink preloadで早期(そうき)ダウンロード
- セルフホスティング: Google Fontsの代(か)わりに直接(ちょくせつ)ホスティング(DNS/接続(せつぞく)コスト削減)
- size-adjust: 代替(だいたい)フォントとWebフォントのサイズを合(あ)わせてCLS防止(ぼうし)
13. クイズ
Q1. LCPの「Good」基準(きじゅん)は?
正解(せいかい): 2.5秒(びょう)以内(いない)
LCP(Largest Contentful Paint)の基準:
- Good: 2.5秒以内
- Needs Improvement: 2.5〜4.0秒
- Poor: 4.0秒超過(ちょうか)
LCPはビューポートで最(もっと)も大(おお)きなコンテンツ要素(ようそ)がレンダリングされる時間(じかん)を測定(そくてい)します。
Q2. FIDを置(お)き換(か)えたCore Web Vitals指標(しひょう)は?
正解: INP(Interaction to Next Paint)
2024年(ねん)3月(がつ)からFID(First Input Delay)がINPに置(お)き換(か)えられました。FIDは最初(さいしょ)のインタラクションのみを測定(そくてい)しましたが、INPはページのライフサイクル全体(ぜんたい)のすべてのインタラクションを測定し、より包括的(ほうかつてき)な応答性(おうとうせい)指標(しひょう)を提供(ていきょう)します。
Q3. HTTPキャッシュにおけるimmutableの意味(いみ)は?
正解: リソースが絶対(ぜったい)に変(か)わらないことを示(しめ)し、再検証(さいけんしょう)リクエストを防止(ぼうし)する
Cache-Control: immutableはリソースが変更(へんこう)されないことをブラウザに伝(つた)えます。これにより、ブラウザがmax-age期間(きかん)内(ない)に再検証(304)リクエストを送(おく)らなくなります。ファイル名(めい)にハッシュが含(ふく)まれる静的(せいてき)アセット(app.abc123.js)に最適(さいてき)です。
Q4. Tree Shakingが動作(どうさ)するための前提条件(ぜんていじょうけん)は?
正解: ES Module(import/export)の使用(しよう)
Tree ShakingはES Moduleの静的(せいてき)構造(こうぞう)を分析(ぶんせき)して、使用(しよう)されていないexportを除去(じょきょ)します。CommonJS(require/module.exports)は動的(どうてき)であるためTree Shakingが不可能(ふかのう)です。package.jsonの"sideEffects": false設定(せってい)も重要(じゅうよう)です。
Q5. Next.js Server Componentsはクライアントバンドルに含(ふく)まれますか?
正解: いいえ、Server ComponentsはクライアントJSバンドルに含まれません
Server Componentsはサーバーでのみ実行(じっこう)され、結果(けっか)のHTMLのみがクライアントに送信(そうしん)されます。これによりJSバンドルサイズを大幅(おおはば)に削減(さくげん)できます。'use client'ディレクティブが宣言(せんげん)されたClient Componentsのみがバンドルに含まれます。
14. 参考(さんこう)資料(しりょう)
公式(こうしき)ドキュメント
測定(そくてい)ツール
画像最適化(がぞうさいてきか)
パフォーマンス参考(さんこう)
- HTTP Archive Web Almanac
- Smashing Magazine Performance
- Addy Osmani - Image Optimization
- Philip Walton - web-vitals