最近学完了公式サイト tutorial(中文官网),入門としては質が良く、チュートリアルの初期コードテンプレートも非常に簡単で、段階を追って Next.js の設計理念や開発モデルに慣れることができ、最終的には Vercel にデプロイされたダッシュボードプロジェクトを実現し、私のデモを見てください 👉 入口
Next.js の概念は非常に多く、公式サイトには詳細な説明があります。新しい技術を学ぶための第一手資料は常に公式文書から得られます。この記事では翻訳作業は行わず、性能 / 体験の最適化に関連する機能をまとめて取り上げます。それでは、本文に入ります。
何が Next.js か#
Next.js は React に基づくフロントエンドアプリケーション開発フレームワークで、高品質かつ高性能なフロントエンドアプリケーションを開発することを目的としています。非常に強力で、公式サイトのように非常にクールです:
- ファイルシステムに基づくルーティング
- UX & CWV 性能最適化関連コンポーネントの内蔵サポート
- 動的 HTML ストリーミングレンダリング
- React の最新機能
- 様々な CSS モードをサポート、CSS モジュール、TailwindCSS、Sass、Less などの様々な CSS バリアント
- C/S/ISR、サーバーコンポーネント、サーバーサイドデータ検証、取得
ちょうど Next.js 2024 開発者サミットが開催されるので、私は仮想入場証を作成しました 😬:
上記で言及した「内蔵性能最適化関連コンポーネント」は主に以下のいくつかを含みます:
画像 - LCP & CLS#
Web Almanac の統計によると、インターネット上の静的リソースの中で、画像リソースの割合は HTML、CSS、Javascript、フォントリソースよりもはるかに高く、画像はしばしばウェブサイトの LCP 性能指標を決定します。したがって、Next.js は <img>
タグを拡張し、next/image コンポーネントを内蔵して以下の最適化を実現しています:
- サイズ最適化:異なるデバイスに対して最新のブラウザ画像フォーマットをサポート:WebP/AVIF
- CLS 最適化:画像読み込み時に事前に幅と高さを占有し、視覚的安定性を保証
- ページの初期読み込み速度を向上:画像の遅延読み込み、ぼかし画像
- サイズの自動適応:デバイスに応じたレスポンシブ画像の表示
使用方法:
// ローカル画像
import Image from 'next/image'
import profilePic from './me.png'
export default function Page() {
return (
<Image
src={profilePic}
alt="著者の写真"
// width={500} 自動的に提供
// height={500} 自動的に提供
// blurDataURL="data:..." 自動的に提供
// placeholder="blur" // 読み込み中のオプションのぼかし
priority // fetchPriority="high" 読み込み優先度を上げる
/>
)
}
注:】await import
または require
はサポートされていません。静的 import
は画像情報をビルド時に分析するためです。
// ネットワーク画像
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="著者の写真"
width={500} // 手動設定
height={500} // 手動設定
/>
)
}
注:】幅と高さを手動で設定する必要があります。
動画#
ベストプラクティス:
- フォールバックコンテンツ:ビデオタグがサポートされていない場合に表示されるバックアップコンテンツ
- 字幕とキャプションの提供:アクセシビリティのサポート
- 互換性のあるビデオコントロール:キーボード操作のビデオコントロール(controls)関連機能をサポート
video リソースを自己ホストする利点:
- 完全に制御可能で、第三者の制限を受けない
- ストレージソリューションの選択自由:高性能で弾力的に縮小可能な CDN を選択してストレージ
- ストレージ容量と帯域幅のバランス
Next.js が提供するソリューション:@vercel/blob
フォント - CLS#
Next.js でフォントを使用する際、ビルド段階でフォントリソースを他の静的リソースと一緒に自分のサーバーにホストします。これにより、Google に追加のネットワークリクエストを発行してフォントをダウンロードする必要がなく、プライバシー保護と性能向上に寄与します。コンポーネントが提供する最適化機能:
- すべての Google Fonts を使用することをサポートし、auto subset によりフォントサイズを削減し、一部の文字セットのみを読み込む
- フォントリソースも同じ主ドメインにデプロイされる
- フォントリソースは追加のリクエストを占有しない
- 複数のフォントを使用する際には必要に応じて読み込むことができる。例えば:特定のページでのみ読み込む、レイアウト範囲内で読み込む、全体で読み込む
使用方法#
- ツール関数式
// app/fonts.ts
import { Inter, Roboto_Mono } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
})
// app/layout.tsx
import { inter } from './fonts'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>
<div>{children}</div>
</body>
</html>
)
}
// app/page.tsx
import { roboto_mono } from './fonts'
export default function Page() {
return (
<>
<h1 className={roboto_mono.className}>私のページ</h1>
</>
)
}
- CSS Variables 式:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${roboto_mono.variable}`}>
<body>{children}</body>
</html>
)
}
// app/global.css
html {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-roboto-mono);
}
- Tailwind CSS と共に:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
plugins: [],
}
メタデータ - SEO#
メタデータは以下の機能を提供します:
- ウェブサイトの TDK(タイトル、説明、キーワード)を簡単に設定
- Open Graph を設定し、ソーシャルプラットフォーム(Facebook、Twitter)での共有情報(タイトル、説明、画像、著者、時間など)を提供
- robots により検索エンジンのクローラーがページをどのように処理するかを制御し、インデックス、トラッキング、キャッシュの有無を設定
- 非同期でメタデータを生成することをサポート
使用方法#
- 静的設定
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {}
- 動的設定
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: { id: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// ルートパラメータを読み取る
const id = params.id
// データを取得
const product = await fetch(`https://.../${id}`).then((res) => res.json())
// 必要に応じて親メタデータをアクセスして拡張(置き換えではなく)
const previousImages = (await parent).openGraph?.images || []
return {
title: product.title,
openGraph: {
images: ['/some-specific-page-image.jpg', ...previousImages],
},
}
}
export default function Page({ params, searchParams }: Props) {}
注:】非同期 generateMetadata
によって生成された Metadata
はサーバーコンポーネント内でのみ設定がサポートされており、Next.js は非同期実行が完了するのを待ってから UI をストリーミングで返します。これにより、ストリーミングの最初の応答が正しい <head>
タグを含むことが保証されます。
スクリプト - LCP & INP#
コンポーネントが提供する最適化機能:
- 第三者依存関係は複数のルートページ間で共有でき、一度だけ読み込まれる
- 依存関係はレイアウト / ページ単位で分割され、必要に応じて読み込まれる
- 様々な読み込み戦略を設定することをサポート:
beforeInteractive
、afterInteractive
、lazyLoad
、worker
(partytown を利用して Web ワーカー内で第三者依存関係を読み込む)
使用方法#
- layout/page スクリプト、特定のレイアウト / ページでのみ第三者依存関係を読み込むことで、その性能への影響を軽減
// app/dashboard/layout.tsx
import Script from 'next/script'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<section>{children}</section>
<Script src="https://example.com/script.js" />
</>
)
}
- 読み込み戦略の調整
beforeInteractive
:ページのハイドレーション前afterInteractive
:ページのハイドレーション後lazyLoad
:ブラウザのアイドル時間に読み込むworker
(実験中):Web ワーカーでの読み込みを開始し、非 CRP の JS はワーカー内で実行されるべきです。
- インラインスクリプトをサポート
id
prop が必須で、Next.js がスクリプトの最適化を追跡できるようにします。
<Script id="show-banner">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>
// OR
<Script
id="show-banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').classList.remove('hidden')`,
}}
/>
- 三つのコールバックをサポート
onLoad
:スクリプトの読み込みが完了onReady
:スクリプトの読み込みが完了した後、毎回コンポーネントがマウントされるonError
:スクリプトの読み込みが失敗
パッケージバンドリング - LCP & INP#
産物分析#
webpack-bundle-analyzer に似て、Next.js も @next/bundle-analyzer
を提供しており、産物分析に使用できます。以下のように使用します:
// pnpm add @next/bundle-analyzer -D
/** @type {import('next').NextConfig} */
const nextConfig = {}
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)
// ANALYZE=true pnpm build
最適化されたインポート#
Next.js が最適化した第三者依存関係のリストに従い、必要に応じてインポートし、全量インポートを避けます。
external#
Next.js が external になった第三者依存関係のリストは、webpack externals に似ており、依存関係を CDN 形式でインポートすることを指定し、ビルド産物のサイズを減少させ、読み込みのタイミングを制御します。
レイジーローディング - LCP/INP#
必要に応じてコンポーネントや第三者依存関係を読み込むことで、ページの読み込み速度を向上させます。
使用方法#
next/dynamic
// app/page.ts
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// クライアントコンポーネント:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
// SSR を明示的に無効化
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
// カスタムローディングコンポーネント
const WithCustomLoading = dynamic(
() => import('../components/WithCustomLoading'),
{
loading: () => <p>読み込み中...</p>,
}
)
export default function ClientComponentExample() {
const [showMore, setShowMore] = useState(false)
return (
<div>
{/* すぐに読み込むが、別のクライアントバンドルで */}
<ComponentA />
{/* 条件が満たされたときのみ、必要に応じて読み込む */}
{showMore && <ComponentB />}
<button onClick={() => setShowMore(!showMore)}>切り替え</button>
{/* クライアント側でのみ読み込む */}
<ComponentC />
{/* カスタムローディング */}
<WithCustomLoading />
</div>
)
}
React.lazy
&Suspense
import()
動的インポート
'use client'
import { useState } from 'react'
const names = ['Tim', 'Joe', 'Bel', 'Lee']
export default function Page() {
const [results, setResults] = useState()
return (
<div>
<input
type="text"
placeholder="検索"
onChange={async (e) => {
const { value } = e.currentTarget
// fuse.js を動的に読み込む
const Fuse = (await import('fuse.js')).default
const fuse = new Fuse(names)
setResults(fuse.search(value))
}}
/>
<pre>結果: {JSON.stringify(results, null, 2)}</pre>
</div>
)
}
- 名前付きエクスポートのインポート
// components/hello.js
'use client'
export function Hello() {
return <p>こんにちは!</p>
}
// app/page.ts
import dynamic from 'next/dynamic'
const HelloComponent = dynamic(() =>
import('../components/hello').then((mod) => mod.Hello)
)
アナリティクス#
内蔵で性能指標の測定と報告をサポートしています。詳細は省略します!!
// app/_components/web-vitals.js
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric)
})
}
// app/layout.js
import { WebVitals } from './_components/web-vitals'
export default function Layout({ children }) {
return (
<html>
<body>
<WebVitals />
{children}
</body>
</html>
)
}
Web Vitals については多くを語る必要はありません。
メモリ最適化#
アプリケーションの進化に伴い、機能が豊富になり、開発とビルドの過程でプロジェクトの実行はますます多くのシステムリソースを消費します。Next.js はいくつかの戦略を提供して最適化を図ります:
experimental.webpackMemoryOptimizations: true
webpack ビルド時の最大メモリ使用量を減少させる、実験段階next build --experimental-debug-memory-usage
ビルド時にメモリ使用状況を印刷node --heap-prof node_modules/next/dist/bin/next build
メモリ問題のトラブルシューティングのためにスタック情報を記録
監視#
- インスツルメンテーション、監視とログツールをプロジェクトに統合
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel('next-app')
}
- 第三者ライブラリ
- Google タグマネージャー
// app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">Google アナリティクス - gtag.js
<GoogleTagManager gtmId="GTM-XYZ" />
<body>{children}</body>
</html>
)
}
- Google アナリティクス - gtag.js
// app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
<GoogleAnalytics gaId="G-XYZ" />
</html>
)
}
- Google マップ埋め込み
- YouTube 埋め込み
レンダリング#
サーバーコンポーネント#
SSR は多くの利点をもたらします:
- データソースに近い(同じデータセンター内)、データ取得が速く、クライアントリクエスト数を減少させる;
- より安全で、敏感なトークンや API キーをネットワーク上で送信する必要がない;
- キャッシュを利用し、次回のリクエストでキャッシュデータを直接返し、再度データ取得やレンダリングのロジックを経る必要がなく、RT を最適化;
- 弱いネットワークや性能が低いデバイスを使用するユーザーの性能体験を向上させ、純粋な表示型 UI をサーバーサイドでレンダリングし、ブラウザが JS コードをダウンロード、解析、実行する時間を効果的に節約;
- より速い初期読み込みと良好な FCP、ブラウザが準備された HTML を直接ダウンロードし、ページ内容を即座に表示し、ホワイトスクリーン時間を減少;
- SEO;
- ストリーミングレンダリング戦略をさらに適用;
デフォルトでは、Next.js のコンポーネントはすべてサーバーコンポーネントです。SSR では以下の API を使用できないことに注意してください:
- イベントリスナー
- DOM/BOM API
- useContext、useEffect、useState など
レンダリング方法#
まずはサーバー側:
- React は React コンポーネントを
React Server Component Payload
(RSC Payload
)にレンダリングします。 - Next.js は
RSC Payload
とクライアントコンポーネントの JavaScript を使用してサーバー側で HTML をレンダリングします(renderToString
?renderToPipeableStream
?)。
クライアント側に到達すると:
- HTML を受け取ったらすぐにページをレンダリング
RSC Payload
を使用してクライアントとサーバーのコンポーネントツリーを調和し、DOM を更新- クライアントコンポーネントをハイドレートし、イベントをバインドしてインタラクティブにします。
サーバーコンポーネントのレンダリング方法は、静的レンダリング、動的レンダリング、ストリームレンダリングに分かれます。
静的レンダリング#
ビルド時にサーバー側で静的コンテンツとしてレンダリングされ、キャッシュされ、次のリクエストに対して直接返されます。純粋な静的表示型 UI やデータが変わらない、ユーザーに差がない UI に適しています。
動的レンダリング#
静的レンダリングとは反対に、データに千人千面の特性がある場合、各リクエストで Cookie や searchParams などのデータを取得する必要がある場合は、毎回リクエスト時にリアルタイムで結果をレンダリングする必要があります。
動的レンダリングと静的レンダリングの切り替えは Next.js が自動的に行い、開発者が使用する API に応じて適切なレンダリング戦略を選択します。例えば、動的関数:
cookies()
headers()
unstable_noStore()
unstable_after()
searchParams prop
これらの動的レンダリング API を使用するルートでは、Next.js は動的レンダリング戦略を採用します。
ストリームレンダリング - TTFB & FCP & FID#
非ストリームレンダリング#
SSR は A、B、C、D のシリアライズされた、順次ブロックされるステップを経なければなりません。サーバーはすべてのデータを取得してから HTML のレンダリングを開始し、クライアントは完全な JS を取得してからハイドレーションを開始します。
ストリームレンダリング#
ページの異なる部分を異なるチャンクに分割し、サーバーが段階的に返します。先に返されたものが先にレンダリングされ、表示され、すべてのデータが準備完了するのを待つ必要はありません。React コンポーネントは本質的に独立したチャンクであり、非同期データ取得に依存しないチャンクは直接返すことができ、残りは順次返されます。
クライアントコンポーネント#
クライアントレンダリングの利点:
- インタラクティブ性を提供し、State、Effect、EventListener を直接記述できる
- BOM、DOM API を直接呼び出すことができる
クライアントコンポーネントを宣言するのは非常に簡単で、ファイルの先頭に'use client'
指令を記述することで Next.js にこのコンポーネントがクライアントコンポーネントであることを知らせるだけです:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>{count} 回クリックしました</p>
<button onClick={() => setCount(count + 1)}>クリックしてください</button>
</div>
)
}
クライアントコンポーネントを宣言しない場合、デフォルトではサーバーコンポーネントとなり、クライアント API を使用するとエラーが発生します:
レンダリング方法#
ページの初期読み込みについては、サーバーコンポーネントと同様です。
その後のナビゲーションについては、クライアント SPA ナビゲーションに似ており、サーバーに新しい HTML を生成するリクエストを発行するのではなく、RSC Payload を利用してナビゲーションを完了します。
いつ SC いつ CC を見る#
PPR#
部分的なプリレンダリング(Partial Prerender)、Next.js はビルド時にできるだけ多くのコンポーネントをプリレンダリングし、Suspense にラップされた非同期コンポーネントに遭遇した場合、フォールバック内の UI も先にプリレンダリングされます。これにより、複数のリクエストを統合し、ブラウザのネットワークリクエストの滝を減少させることができます。
ローディング#
loading.tsx
は Next.js における特別なファイルで、Suspense
に基づいてルーティングやコンポーネントレベルのローディング状態を実現します。SkeletonComponent
や SpinnerComponent
を実装して非同期コンポーネントの読み込み時のフォールバック UI として使用することができ、UX を効果的に向上させることができます。
最後に#
Next.js の公式サイトには非常に多くの設計哲学や理念があり、繰り返し考察し、思考する価値があります。最近、Twitter でも多くの独立開発者が Next.js を基に迅速に製品を開発し、自分のアイデアを具現化しているのを見かけました。これは開発、デプロイ、性能からバックエンドまで一貫したサービスを提供する次世代のウェブフレームワークで、全栈的な味わいがあります。
完結