こんにちは!前回のエピソードでは、Stripeを統合して安全な決済機能を追加しました。今回は、アプリケーションのパフォーマンスを最適化し、GoogleのCore Web Vitals(LCP、CLS、FID)を向上させます。LighthouseやVercel Analyticsを使った分析を通じて、ユーザー体験をさらに向上させ、SEOスコアを高めましょう。React Server Componentsを活用してクライアントサイドのJavaScriptを削減する方法も紹介します!
このエピソードのゴール
- LighthouseとVercel Analyticsでパフォーマンスを分析。
- LCP(Largest Contentful Paint)をフォント最適化と遅延読み込みで改善。
- CLS(Cumulative Layout Shift)をレイアウト安定化で削減。
- React Server Componentsを活用してJavaScriptの量を削減。
- Lighthouseスコアを90以上にする。
必要なもの
- 前回のプロジェクト(
next-ecommerce
)がセットアップ済み。 - Google Chrome(Lighthouseを使用)。
- Vercelアカウント(Analyticsを使用する場合)。
- 基本的なTypeScript、React、Next.jsの知識。
ステップ1: パフォーマンスの分析
まず、現在のアプリケーションのパフォーマンスをLighthouseで分析します。Google Chromeのデベロッパーツールを開き、以下の手順を実行:
-
Lighthouseレポートの生成:
- Chromeで
http://localhost:3000
にアクセス。 - デベロッパーツール(F12)を開き、「Lighthouse」タブを選択。
- 「Performance」と「SEO」をチェックし、「Generate report」をクリック。
- Chromeで
-
Vercel Analyticsの設定:
- Vercelにプロジェクトをデプロイ済みの場合、VercelダッシュボードでAnalyticsを有効化。
- 実際のユーザーアクセスデータ(ページ読み込み時間、デバイス別パフォーマンスなど)を確認。
分析結果から、LCP(Largest Contentful Paint)、CLS(Cumulative Layout Shift)、FID(First Input Delay)などの指標を確認します。目標は、Lighthouseスコアを90以上にすることです。
ステップ2: LCP(Largest Contentful Paint)の最適化
LCPは、ページの主要コンテンツが表示されるまでの時間を測定します。商品一覧ページ(PLP)と商品詳細ページ(PDP)を対象に、以下の最適化を行います。
フォントの最適化
システムフォントを使用している場合でも、カスタムフォントを追加するとLCPが遅延する可能性があります。src/styles/globals.css
を更新し、フォントの読み込みを最適化:
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* カスタムフォントの読み込み(必要に応じて) */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap; /* フォント読み込み中のテキスト表示を最適化 */
}
font-display: swap
を指定することで、フォントが読み込まれる前に代替フォントを表示し、LCPを改善します。
画像の遅延読み込み
商品一覧ページ(src/app/page.tsx
)の画像にloading="lazy"
を追加し、ビューポート外の画像を遅延読み込みします:
<Image
src={product.image}
alt={product.title}
width={200}
height={200}
className="w-full h-48 object-cover rounded"
loading="lazy" // 遅延読み込みを有効化
/>
PDP(src/app/products/[handle]/page.tsx
)では、最初の画像にpriority
を保持し、以降の画像にloading="lazy"
を適用:
<Image
key={index}
src={image.node.url}
alt={image.node.altText || product.title}
width={500}
height={500}
className="w-full h-auto object-cover rounded"
priority={index === 0}
loading={index === 0 ? undefined : 'lazy'}
/>
ステップ3: CLS(Cumulative Layout Shift)の削減
CLSは、ページのレイアウトが予期せず移動する問題を測定します。以下の方法でCLSを削減します。
画像のアスペクト比を固定
画像に幅と高さを明示的に指定することで、読み込み時のレイアウトシフトを防ぎます。src/app/page.tsx
の画像にstyle
を追加:
<Image
src={product.image}
alt={product.title}
width={200}
height={200}
className="w-full h-48 object-cover rounded"
loading="lazy"
style={{ aspectRatio: '1 / 1' }} // アスペクト比を固定
/>
スケルトンローディングの実装
商品データの読み込み中にスケルトンUIを表示して、CLSを防ぎます。src/components/ProductSkeleton.tsx
を作成:
export default function ProductSkeleton() {
return (
<div className="border rounded-lg p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded"></div>
<div className="h-6 bg-gray-200 mt-2 rounded"></div>
<div className="h-4 bg-gray-200 mt-2 rounded w-1/2"></div>
</div>
);
}
src/app/page.tsx
を更新してスケルトンUIを表示:
import ProductSkeleton from '@/components/ProductSkeleton';
// ... 既存のコード ...
export default function Home({ products }: HomeProps) {
return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">商品一覧</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{products.length === 0
? Array(6)
.fill(0)
.map((_, index) => <ProductSkeleton key={index} />)
: products.map((product) => (
<Link href={`/products/${product.handle}`} key={product.id}>
<div className="border rounded-lg p-4 hover:shadow-lg transition">
<Image
src={product.image}
alt={product.title}
width={200}
height={200}
className="w-full h-48 object-cover rounded"
loading="lazy"
style={{ aspectRatio: '1 / 1' }}
/>
<h2 className="text-xl font-semibold mt-2">{product.title}</h2>
<p className="text-gray-600">
{parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2)}{' '}
{product.priceRange.minVariantPrice.currencyCode}
</p>
</div>
</Link>
))}
</div>
</main>
);
}
これで、データ取得中にスケルトンUIが表示され、CLSが削減されます。
ステップ4: React Server Componentsの活用
React Server Components(RSC)は、サーバー側でコンポーネントをレンダリングし、クライアントに送信するJavaScriptを削減します。商品一覧ページをRSCとして書き換えます。src/app/page.tsx
を更新:
import { getProducts } from '@/lib/shopify';
import Link from 'next/link';
import Image from 'next/image';
interface Product {
id: string;
title: string;
handle: string;
priceRange: {
minVariantPrice: {
amount: string;
currencyCode: string;
};
};
images: {
edges: Array<{
node: {
url: string;
altText: string | null;
};
}>;
};
}
export default async function Home() {
const products = await getProducts(10); // サーバー側で直接データ取得
return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">商品一覧</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{products.map((product) => (
<Link href={`/products/${product.handle}`} key={product.id}>
<div className="border rounded-lg p-4 hover:shadow-lg transition">
<Image
src={product.images.edges[0]?.node.url}
alt={product.images.edges[0]?.node.altText || product.title}
width={200}
height={200}
className="w-full h-48 object-cover rounded"
loading="lazy"
style={{ aspectRatio: '1 / 1' }}
/>
<h2 className="text-xl font-semibold mt-2">{product.title}</h2>
<p className="text-gray-600">
{parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2)}{' '}
{product.priceRange.minVariantPrice.currencyCode}
</p>
</div>
</Link>
))}
</div>
</main>
);
}
このコードは:
-
getStaticProps
を削除し、サーバー側で直接getProducts
を呼び出す。 - RSCとしてレンダリングされ、クライアントに送信されるJavaScriptを削減。
- 静的生成(SSG)とISRは引き続き有効。
ステップ5: 動作確認
- 開発サーバーを起動(
npm run dev
)。 -
http://localhost:3000
と/products/[handle]
にアクセスし、以下の点を確認:- ページ読み込みが高速(LCPが2.5秒未満)。
- レイアウトシフトが発生しない(CLSが0.1未満)。
- スケルトンUIがデータ取得中に表示される。
- Lighthouseで再度分析し、Performanceスコアが90以上であることを確認。
- デベロッパーツールのネットワークタブで、JavaScriptのサイズが削減されていることを確認。
エラーがあれば、コンソールログやLighthouseの提案を確認してください。
まとめと次のステップ
このエピソードでは、LighthouseとVercel Analyticsを使ってパフォーマンスを分析し、LCPとCLSを最適化しました。フォント最適化、遅延読み込み、スケルトンUI、React Server Componentsを活用することで、Core Web Vitalsを大幅に改善し、Lighthouseスコア90以上を達成しました。
次回のエピソードでは、Algoliaを使った検索とフィルタリング機能を追加します。商品を素早く見つけるためのオートコンプリートやカテゴリフィルタも実装しますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!