1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsで高速eコマースアプリを構築する | エピソード6: パフォーマンス最適化とCore Web Vitals

Posted at

こんにちは!前回のエピソードでは、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のデベロッパーツールを開き、以下の手順を実行:

  1. Lighthouseレポートの生成

    • Chromeでhttp://localhost:3000にアクセス。
    • デベロッパーツール(F12)を開き、「Lighthouse」タブを選択。
    • 「Performance」と「SEO」をチェックし、「Generate report」をクリック。
  2. 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: 動作確認

  1. 開発サーバーを起動(npm run dev)。
  2. http://localhost:3000/products/[handle]にアクセスし、以下の点を確認:
    • ページ読み込みが高速(LCPが2.5秒未満)。
    • レイアウトシフトが発生しない(CLSが0.1未満)。
    • スケルトンUIがデータ取得中に表示される。
  3. Lighthouseで再度分析し、Performanceスコアが90以上であることを確認。
  4. デベロッパーツールのネットワークタブで、JavaScriptのサイズが削減されていることを確認。

エラーがあれば、コンソールログやLighthouseの提案を確認してください。


まとめと次のステップ

このエピソードでは、LighthouseとVercel Analyticsを使ってパフォーマンスを分析し、LCPとCLSを最適化しました。フォント最適化、遅延読み込み、スケルトンUI、React Server Componentsを活用することで、Core Web Vitalsを大幅に改善し、Lighthouseスコア90以上を達成しました。

次回のエピソードでは、Algoliaを使った検索とフィルタリング機能を追加します。商品を素早く見つけるためのオートコンプリートやカテゴリフィルタも実装しますので、引き続きお楽しみに!


この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?