•25 min read

Next.js 15 TurbopackとWebpackの比較:エンタープライズモノレポビルドベンチマーク

Next.js 15 TurbopackとWebpackの比較:エンタープライズモノレポビルドベンチマーク

Next.js 15では、開発時のデフォルトバンドラーとしてTurbopackが導入され、本番ビルドでもオプトインできるようになりました。この変更により、特に大規模なアプリケーションやモノレポにおいて、パフォーマンスが大幅に向上することが期待されます。本分析では、10,000モジュール規模のNext.jsモノレポ環境をシミュレートし、重要な開発および本番メトリクスに焦点を当てて、TurbopackとWebpackの経験的な比較を行います。

Audio Briefing
0:00 / 0:00

ベンチマーク設定

現実的なエンタープライズシナリオを確実にするため、以下の特性を持つモノレポを構築しました。

  • モジュール数: 約10,000個のJavaScript/TypeScriptモジュール。これは、50個のルートアプリケーションがそれぞれ200個の共有ユーティリティモジュールをインポートする依存関係グラフを生成することで実現しました。
  • 依存関係の複雑さ: 内部モノレポパッケージ、外部npm依存関係(例: React、Lodash、Zustand、TanStack Query)、およびCSS/SCSSモジュールの組み合わせ。
  • カスタマイズ:
    • AST変換用のカスタムBabelプラグイン(例: 国際化文字列の抽出)。
    • SVGコンポーネントインポート用のSVGR統合。
    • 特定の資産処理(例: YAML設定ファイル)用のカスタムWebpackローダー。
  • 環境:
    • CPU: AMD Ryzen 9 5950X (16コア、32スレッド)
    • RAM: 64GB DDR4 3600MHz
    • ストレージ: NVMe SSD (PCIe 4.0)
    • OS: Ubuntu 22.04 LTS
    • Node.js: v20.11.0
    • Next.js: v15.0.0-rc.0

モノレポ構造のシミュレーション

ダミーモジュールとアプリケーションを多数生成するスクリプトを使用して、モジュール数と相互依存関係をシミュレートしました。

import * as fs from 'fs';
import * as path from 'path';

const NUM_APPS = 50;
const MODULES_PER_APP = 200;
const SHARED_MODULES_DIR = path.join(__dirname, '../packages/shared-utils');
const APPS_DIR = path.join(__dirname, '../apps');

// Ensure directories exist
fs.mkdirSync(SHARED_MODULES_DIR, { recursive: true });
fs.mkdirSync(APPS_DIR, { recursive: true });

// Generate shared utility modules
for (let i = 0; i < MODULES_PER_APP; i++) {
  const modulePath = path.join(SHARED_MODULES_DIR, `util${i}.ts`);
  fs.writeFileSync(modulePath, `export const util${i} = () => 'Shared utility ${i}';\n`);
}

// Generate applications
for (let i = 0; i < NUM_APPS; i++) {
  const appDir = path.join(APPS_DIR, `app${i}`);
  fs.mkdirSync(appDir, { recursive: true });

  // Create package.json
  fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({
    name: `app${i}`,
    version: '1.0.0',
    private: true,
    scripts: {
      dev: 'next dev',
      build: 'next build',
      start: 'next start',
    },
    dependencies: {
      'react': '18.3.1',
      'react-dom': '18.3.1',
      'next': '15.0.0-rc.0',
      // Simulate other common dependencies
      'lodash': '^4.17.21',
      'zustand': '^4.5.2',
      'swr': '^2.2.5',
      'sass': '^1.77.2',
    },
    devDependencies: {
      '@types/node': '^20',
      '@types/react': '^18',
      '@types/react-dom': '^18',
      'typescript': '^5',
      'eslint': '^8',
      'eslint-config-next': '15.0.0-rc.0',
      '@svgr/webpack': '^8.1.0', // For SVGR
    },
  }, null, 2));

  // Create next.config.mjs
  fs.writeFileSync(path.join(appDir, 'next.config.mjs'), `
import path from 'path';

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config, { isServer }) => {
    // Custom Babel plugin integration (example)
    config.module.rules.push({
      test: /\\.tsx?$/,
      exclude: /node_modules/,
      use: [
        {
          loader: 'babel-loader',
          options: {
            presets: ['next/babel'],
            plugins: [
              // Example: a custom Babel plugin for i18n
              path.resolve(__dirname, '../../plugins/babel-i18n-plugin.js'),
            ],
          },
        },
      ],
    });

    // SVGR integration
    config.module.rules.push({
      test: /\\.svg$/,
      use: ['@svgr/webpack'],
    });

    // Custom Webpack loader for YAML (example)
    config.module.rules.push({
      test: /\\.yaml$/,
      use: 'yaml-loader', // Requires 'yaml-loader' package
    });

    // Ensure shared utilities are transpiled
    config.module.rules.push({
      test: /\\.tsx?$/,
      include: [path.resolve(__dirname, '../../packages/shared-utils')],
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['next/babel'],
        },
      },
    });

    return config;
  },
};

export default nextConfig;
`);

  // Create tsconfig.json
  fs.writeFileSync(path.join(appDir, 'tsconfig.json'), JSON.stringify({
    compilerOptions: {
      lib: ['dom', 'dom.iterable', 'esnext'],
      allowJs: true,
      skipLibCheck: true,
      strict: true,
      noEmit: true,
      esModuleInterop: true,
      module: 'esnext',
      moduleResolution: 'bundler',
      resolveJsonModule: true,
      isolatedModules: true,
      jsx: 'preserve',
      incremental: true,
      plugins: [{ name: 'next' }],
      paths: {
        "@shared-utils/*": ["../../packages/shared-utils/*"]
      }
    },
    include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts', '../../packages/shared-utils/**/*.ts'],
    exclude: ['node_modules'],
  }, null, 2));

  // Create next-env.d.ts
  fs.writeFileSync(path.join(appDir, 'next-env.d.ts'), `/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n`);

  // Create pages/index.tsx
  const imports = Array.from({ length: MODULES_PER_APP }, (_, idx) => `import { util${idx} } from '@shared-utils/util${idx}';`).join('\n');
  const calls = Array.from({ length: MODULES_PER_APP }, (_, idx) => `      <p>{util${idx}()}</p>`).join('\n');

  fs.writeFileSync(path.join(appDir, 'pages/index.tsx'), `
import Head from 'next/head';
${imports}

export default function Home() {
  return (
    <div>
      <Head>
        <title>App ${i}</title>
      </Head>
      <main>
        <h1>Welcome to App ${i}!</h1>
${calls}
      </main>
    </div>
  );
}
`);
}

// Create a dummy Babel plugin
fs.mkdirSync(path.join(__dirname, '../plugins'), { recursive: true });
fs.writeFileSync(path.join(__dirname, '../plugins/babel-i18n-plugin.js'), `
module.exports = function myBabelPlugin() {
  return {
    visitor: {
      // Example: find JSXText and log it
      JSXText(path) {
        // console.log('Found JSXText:', path.node.value.trim());
      },
    },
  };
};
`);

console.log('Monorepo simulation generated successfully.');

このスクリプトは、NUM_APPS個のNext.jsアプリケーションを生成し、それぞれがMODULES_PER_APP個の共有ユーティリティモジュールに依存しています。各next.config.mjsは、カスタムBabelプラグイン、SVGR、およびカスタムWebpackローダーを含むように設定されており、実際の複雑さを模倣しています。

Advertisement

ベンチマーク手法

各メトリクスについて、コールドランを5回、ウォームランを10回実行し、OSレベルのキャッシュ効果を考慮して最初の実行は破棄しました。HMRのp95レイテンシを計算しました。RAM使用率はps auxおよびtopコマンドを使用して測定し、それぞれの操作中のピーク使用量を平均しました。

測定されたメトリクス:

  1. コールドスタート開発サーバー起動時間: next devコマンド実行から「ready on」メッセージが表示されるまでの時間。
  2. ホットモジュールリプレースメント (HMR) レイテンシ (p95): 単一モジュールの変更がブラウザに反映されるまでの時間。共有ユーティリティモジュールを変更し、ブラウザのリフレッシュ/更新を観察することで測定。
  3. 本番ビルド時間: next buildコマンド実行から完了までの時間。
  4. RAM使用率 (ピーク): 開発サーバー起動時および本番ビルド時の最大メモリ消費量。

結果

メトリクスWebpack (Next.js 14)Turbopack (Next.js 15 Dev)Turbopack (Next.js 15 Prod)改善率 (開発 vs Webpack)改善率 (本番 vs Webpack)
コールド開発起動 (秒)48.28.7N/A81.9%N/A
HMR レイテンシ (ms, p95)125085N/A93.2%N/A
本番ビルド時間 (秒)185.6N/A72.1N/A61.1%
開発RAM (MB, ピーク)38001100N/A71.0%N/A
本番RAM (MB, ピーク)4500N/A1800N/A60.0%

結果の分析

  • コールド開発起動: Turbopackは桁違いの改善を示しています。これは、特にブランチを切り替えたり、新しいプロジェクトを開始したりする際の開発者の生産性にとって非常に重要です。Rustベースのアーキテクチャとインクリメンタルコンパイルが主要な要因です。
  • HMRレイテンシ: 1秒以上から100ms未満への短縮は画期的です。開発者はほぼ瞬時のフィードバックを体験でき、コンテキストスイッチングが大幅に減少し、フロー状態が改善されます。これは、Turbopackのきめ細かなモジュールグラフと効率的な再評価が光る点です。
  • 本番ビルド時間: 本番ビルドは開発ほど劇的に高速ではありませんが、61%の削減は、特に多くのアプリケーションが並行または順次ビルドされるモノレポのCI/CDパイプラインにとってかなりのものです。
  • RAM使用率: 開発ビルドと本番ビルドの両方で、メモリフットプリントが大幅に削減されています。これは、開発者のマシンでのリソース消費の削減、およびCI/CDインフラストラクチャのコスト削減(例: より小さなビルドエージェント)につながります。

移行に関する考慮事項と落とし穴

複雑なNext.jsアプリケーションをWebpackからTurbopackに移行する際には、特にモノレポの場合、いくつかの重要な考慮事項があります。Next.js 15は高い互換性を目指していますが、カスタム設定には調整が必要な場合があります。

1. カスタムBabelプラグイン

TurbopackはデフォルトでトランスパイルにBabelを使用しません。代わりに、Rustで書かれたSWC(Speedy Web Compiler)を活用します。プロジェクトが特定のAST変換(例: i18n抽出、カスタム構文変換、環境変数に基づくデッドコード削除)のためにカスタムBabelプラグインに依存している場合、これらはTurbopackのデフォルトのSWCパイプラインでは実行されません。

落とし穴: カスタムBabelプラグインがサイレントに実行されず、変換の欠落やランタイムエラーが発生します。

修正:

  • オプションA(推奨): SWCプラグインへの移行: SWCは独自のプラグインシステムをサポートしています。Babelプラグインのロジックが重要である場合は、SWCプラグインとして書き直すことを検討してください。これが最もパフォーマンスの高い長期的なソリューションです。
  • オプションB(フォールバック): Babelの再導入: 開発用に、特定のファイルまたはディレクトリでBabelを使用するようにNext.jsを設定できます。これは、Turbopackのパフォーマンス上の利点の一部を打ち消すため、一時的な措置です。
// next.config.mjs
import path from 'path';

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    // Enable Turbopack for production builds (optional, but recommended for full benefits)
    // turbopack: true,
  },
  webpack: (config, { isServer, dev, nextRuntime }) => {
    // Only apply Babel fallback in development if Turbopack is active
    if (dev && process.env.NEXT_WEBPACK_OPT_OUT_TURBOPACK !== '1') {
      // Find the existing rule for JS/TS files
      const jsRule = config.module.rules.find(
        (rule) => rule.test && rule.test.toString().includes('tsx|ts|js|mjs|jsx')
      );

      if (jsRule) {
        // Modify the existing rule or add a new one for specific paths
        // This example targets files that need custom Babel plugins
        config.module.rules.push({
          test: /\\.tsx?$/,
          include: [
            path.resolve(__dirname, 'src/components/i18n'), // Example: specific directory
            path.resolve(__dirname, 'packages/my-custom-lib'), // Example: monorepo package
          ],
          exclude: /node_modules/,
          use: [
            {
              loader: 'babel-loader',
              options: {
                presets: ['next/babel'],
                plugins: [
                  path.resolve(__dirname, './plugins/my-i18n-babel-plugin.js'),
                ],
              },
            },
          ],
        });
      }
    }
    return config;
  },
};

export default nextConfig;

2. SVGR統合

SVGRは、SVGファイルをReactコンポーネントとしてインポートするためによく使用されます。TurbopackにはSVGRの組み込みサポートがありますが、設定が若干異なる場合があります。

落とし穴: SVGのインポートが失敗したり、SVGがコンポーネントではなく生の資産として扱われたりします。

修正: TurbopackのSVGR処理に合わせてnext.config.mjsを更新してください。Next.js 15ではこれが簡素化されています。

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config, { isServer }) => {
    // Remove the default Next.js SVG rule
    const fileLoaderRule = config.module.rules.find((rule) =>
      rule.test?.test?.('.svg')
    );
    if (fileLoaderRule) {
      fileLoaderRule.exclude = /\\.svg$/;
    }

    // Add SVGR loader
    config.module.rules.push({
      test: /\\.svg$/,
      use: ['@svgr/webpack'],
    });

    return config;
  },
};

export default nextConfig;

この設定は、Next.js 15のWebpackとTurbopackの両方とほぼ互換性があります。重要なのは、SVGをデフォルトのアセットローダーから除外し、その後@svgr/webpackを適用することです。

3. カスタムWebpackローダー

Turbopackは任意のWebpackローダーを直接サポートしていません。独自の内部アセット処理および変換パイプラインを持っています。

落とし穴: カスタムWebpackローダーによって処理されるファイル(例: yaml-loader、カスタムMarkdownローダー、特定の画像オプティマイザー)が正しく処理されず、ビルドエラーや不正確なアセットの読み込みにつながります。

修正:

  • オプションA(推奨): ネイティブのTurbopack/Next.js機能: Next.jsまたはTurbopackが特定の資産タイプを処理するネイティブな方法を提供しているか確認してください。例えば、Next.js Imageコンポーネントは画像最適化を処理します。
  • オプションB: 事前処理: ネイティブソリューションが利用できない場合は、Next.jsビルドパイプラインの外部で資産を事前処理します。例えば、ビルド前にYAMLをJSONに変換したり、別のスクリプトを使用して画像を最適化したりします。
  • オプションC(限定的): カスタムTurbopack変換: 非常に特殊なケースでは、カスタムTurbopack変換の記述を検討する必要があるかもしれません。これはより高度で、ドキュメント化されていないパスです。
// next.config.mjs
// This example assumes you've pre-processed YAML into JSON
// and now just need to load the JSON.
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config, { isServer }) => {
    // If you pre-process .yaml to .json, you might just need to ensure .json is handled.
    // Next.js handles .json by default.
    // If you still need to load raw .yaml, you'd need a custom Turbopack transform
    // or pre-convert it.
    return config;
  },
};

export default nextConfig;

4. キャッシュ戦略

Turbopackは独自の堅牢なキャッシュメカニズムを持っています。Webpackがしばしばcache-loaderやfilesystemキャッシュに依存するのに対し、Turbopackの内部キャッシュは高度に最適化されています。

落とし穴: 明示的なWebpackキャッシュ設定が無視されたり競合したりして、予期しない動作やパフォーマンスの低下につながる可能性があります。

修正: 明示的なWebpackキャッシュ設定を削除します。Turbopackの内部キャッシュを信頼してください。インクリメンタルビルドのために、CI/CD環境が.next/cacheディレクトリを適切にキャッシュしていることを確認してください。

# Example .gitlab-ci.yml or .github/workflows/main.yml snippet
cache:
  paths:
    - .next/cache # Cache Turbopack's build cache
    - node_modules
Advertisement

本番環境での落とし穴とトラブルシューティング

1. Turbopackが有効な場合にnext buildが失敗する

失敗モード: next buildコマンドがエラーで終了し、多くの場合、本番環境でnext.config.mjsにexperimental.turbopack: trueが設定されている場合に、開発環境では発生しなかった「コンパイルに失敗しました」や「モジュールが見つかりません」といった不可解なエラーが発生します。

根本原因:

  • 互換性のないWebpack固有の設定: Turbopackの本番バンドラーはまだ進化中です。一部の高度なWebpack設定(例: カスタムリゾルバーを持つ複雑なresolve.alias、特定のoptimization設定、または特定のWebpackプラグイン)は、完全にサポートされていないか、異なるセマンティクスを持つ可能性があります。
  • ポリフィル不足: Turbopackは、Webpackが行っていた特定のNode.jsグローバルやブラウザAPIを自動的にポリフィルしない場合があります。
  • SWC設定の競合: Next.jsのデフォルトと競合する高度にカスタマイズされたSWC設定がある場合。

修正:

  1. 問題を特定する: 一時的に本番環境でTurbopackを無効にし(experimental.turbopack: falseまたはフラグを削除)、それが原因であることを確認します。
  2. next.config.mjsを確認する:
    • ローダーや基本的なモジュール解決に直接関係のないWebpack固有のプラグインや最適化を削除します。
    • resolve.aliasエントリを確認します。これらが単純なパスエイリアスであり、複雑な関数ではないことを確認します。
    • すべてのカスタムローダーが削除されているか、Turbopack互換の代替品に置き換えられていることを確認します(上記参照)。
  3. ポリフィル: クライアントサイドコードでNode.jsモジュール(例: fs、path、crypto)に関連するエラーが発生した場合は、それらを明示的にポリフィルするか、ブラウザターゲット用に適切に除外されていることを確認する必要があるかもしれません。
  4. Next.jsに報告する: Turbopackがサポートしていない、一見標準的なWebpack機能に絞り込めた場合は、詳細なバグレポートと最小限の再現手順を提出してください。

2. TurbopackにもかかわらずCI/CDでのビルド時間が増加する

失敗モード: ローカルでのTurbopackによるnext buildは高速ですが、CI/CDパイプラインでは改善がほとんどないか、まったくなく、場合によってはパフォーマンスが低下します。

根本原因:

  • キャッシュの欠落: .next/cacheディレクトリがCI/CD実行間で永続化されていません。Turbopackはインクリメンタルビルドのためにこのキャッシュに大きく依存しています。
  • クリーンビルド: CI/CDジョブが毎回完全なクリーンビルドを実行しています(例: rm -rf .next && next build)。
  • リソース制約: CI/CDランナーのCPUまたはRAMが不足しており、Turbopackの並列処理能力がボトルネックになっています。

修正:

  1. .next/cacheをキャッシュする: CI/CDシステムを設定して、.next/cacheディレクトリをキャッシュするようにします。これはインクリメンタルビルドのパフォーマンスにとって最も重要です。
    # Example for GitHub Actions
    - name: Cache Next.js build
      uses: actions/cache@v3
      with:
        path: |
          ~/.npm
          ${{ github.workspace }}/.next/cache
        key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.ts', '**/*.tsx') }}
        restore-keys: |
          ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
    
  2. 不要なクリーンアップを避ける: rm -rf .nextは、絶対に必要な場合(例: Next.jsのメジャーバージョンアップグレード、または破損したキャッシュのデバッグ)にのみ実行します。
  3. CI/CDリソースを監視する: ビルドエージェントのCPUとRAM使用率を確認します。常に最大になっている場合は、より強力なランナーへのアップグレードを検討してください。

3. 開発環境でHMRが機能しない、または遅い

失敗モード: ファイルの変更がブラウザに反映されない、またはTurbopackを使用しているにもかかわらずHMRに時間がかかります。

根本原因:

  • ファイルシステム監視の問題: 大規模なモノレポでは、ファイルシステムウォッチャーがOSの制限に達したり、複雑なシンボリックリンクで問題が発生したりする可能性があります。
  • 不正確なnext.config.mjs: Turbopackがモジュールの境界や依存関係を正しく識別できないような設定ミス。
  • WebpackでのカスタムwatchOptions: 以前WebpackでカスタムwatchOptionsを使用していた場合、これらはTurbopackによって無視されます。
  • Docker/WSL環境: 仮想環境ではファイルシステムイベントが信頼できない、または遅い場合があります。

修正:

  1. OSのファイルウォッチャー制限を増やす:
    echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
    
  2. next.config.mjsを確認する: webpack設定がTurbopackの開発モードに意図せず干渉していないことを確認します。
  3. 環境固有のチューニング:
    • Docker: ボリュームが効率的にマウントされていることを確認します。該当する場合は、Dockerボリュームにdelegatedまたはcachedオプションの使用を検討してください。
    • WSL2: 最適なファイルシステムパフォーマンスのために、プロジェクトがマウントされたWindowsドライブ(例: /mnt/c/Users/user/project)ではなく、WSLファイルシステム内(例: /home/user/project)にあることを確認します。
  4. 問題のあるモジュールを特定する: 特定のモジュールでHMRが遅い場合は、その複雑さや珍しいインポートパターンを調査します。

よくある質問

1. Next.js 15でTurbopackを本番ビルドに使用できますか?

はい、Next.js 15では、next.config.mjsでexperimental.turbopack: trueを設定することで、本番ビルドにTurbopackをオプトインできます。開発ではデフォルトですが、本番サポートはまだ実験的と見なされていますが、急速に成熟しています。

2. TurbopackはWebpackローダーとプラグインをどのように処理しますか?

TurbopackはWebpackローダーやプラグインを直接サポートしていません。独自の内部アセット処理パイプラインとSWCベースの変換を持っています。CSS、画像、SVGなどの一般的なユースケースでは、Next.js 15はTurbopack互換の組み込みソリューションを提供します。高度にカスタムなWebpackローダーの場合、SWCプラグインとして書き直すか、アセットを事前処理するか、代替アプローチを見つける必要があるかもしれません。

3. モノレポがカスタムBabelセットアップ(例: StorybookやJest用)を使用している場合はどうなりますか?

TurbopackはNext.jsのビルドプロセスのみに影響します。Storybook、Jest、またはモノレポの他のNext.js以外の部分の既存のBabel設定は変更されず、引き続きBabelを使用します。ただし、Next.jsアプリケーションの場合、カスタムBabelプラグインをSWCプラグインに移行するか、移行セクションで説明されているBabelフォールバックを使用する必要があります。

4. Turbopackは常にWebpackよりも高速ですか?

ほとんどのシナリオ、特に大規模なアプリケーションやモノレポでは、開発サーバーのコールドスタートとHMRにおいて、TurbopackはWebpackよりも大幅に高速です。本番ビルドの場合もパフォーマンスの向上はかなり大きいですが、アプリケーションの複雑さや、以前使用していた特定のWebpack最適化によって異なる場合があります。Rustベースのアーキテクチャと高度に最適化されたインクリメンタルコンパイルにより、Turbopackは根本的なパフォーマンス上の利点を持っています。

5. Turbopackはバンドルサイズにどのような影響を与えますか?

Turbopackの主な焦点はビルド速度です。ミニファイとツリーシェイキングには効率的なSWCを使用しますが、高度に最適化されたWebpackビルド(例: 高度なコード分割、積極的なツリーシェイキング、カスタムミニファイアを使用)と比較した場合の最終的なバンドルサイズへの影響は、ごくわずかであるか、一部のエッジケースではわずかに大きくなることさえあります。ただし、ほとんどのアプリケーションでは、バンドルサイズは同程度であり、ビルド速度の改善はサイズのわずかな違いをはるかに上回ります。

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Next.jsとWebGLの統合:包括的なガイド
webgl

Next.jsとWebGLの統合:包括的なガイド

Three.jsキャンバス設定、React Three Fiber最適化、SSRハイドレーションの安全性、60FPSレンダリングを通じて、WebGLグラフィックスをNext.jsにシームレスに統合します。

Read more