# 機能設計書 60-next/script（Script）

## 概要

本ドキュメントは、Next.jsの`next/script`（`<Script>`コンポーネント）の機能設計を記述する。`<Script>`コンポーネントはサードパーティスクリプトの読み込みを最適化し、4つの読み込み戦略（beforeInteractive、afterInteractive、lazyOnload、worker）を通じてページパフォーマンスへの影響を最小化する。

### 本機能の処理概要

**業務上の目的・背景**：Webアプリケーションでは、アナリティクス、広告、チャットウィジェット、A/Bテストなど多数のサードパーティスクリプトを使用する。これらのスクリプトはページの初期読み込み速度に大きく影響し、Core Web Vitalsを悪化させる原因となる。`<Script>`コンポーネントは、スクリプトの読み込みタイミングを制御する戦略パターンを提供し、重要度に応じた最適なタイミングでスクリプトを実行することで、メインコンテンツの表示速度を維持する。

**機能の利用シーン**：Google Analyticsの統合（afterInteractive）、クッキー同意バナー（lazyOnload）、重要なポリフィル/ABテスト（beforeInteractive）、Web Worker経由の非同期処理（worker/Partytown統合）。

**主要な処理内容**：
1. 4つの読み込み戦略に基づくスクリプト読み込みタイミング制御
2. ScriptCache/LoadCacheによるスクリプトの重複読み込み防止
3. onLoad/onReady/onErrorコールバックによるスクリプトライフサイクル管理
4. App Router/Pages Router両対応のスクリプト注入
5. 関連スタイルシートの読み込み（stylesheets属性）
6. nonceサポートによるCSP対応
7. beforeInteractiveスクリプトのSSR対応（App Router: `self.__next_s`パターン）

**関連システム・外部連携**：HeadManagerContext（Pages Router）、ReactDOM.preload/preinit（App Router）、Partytown（worker戦略）。

**権限による制御**：特にロールや権限による制御は行われない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 2 | ドキュメント (_document) | 補助機能 | beforeInteractive戦略のスクリプト読み込み管理 |

## 機能種別

UIコンポーネント / スクリプト読み込み最適化

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| src | string | Conditional | 外部スクリプトURL（インラインスクリプトでない場合必須） | - |
| id | string | No | スクリプト識別子（重複読み込み防止用キー） | - |
| strategy | string | No | 読み込み戦略（デフォルト: 'afterInteractive'） | 'beforeInteractive','afterInteractive','lazyOnload','worker' |
| onLoad | function | No | スクリプト読み込み完了コールバック | 関数型 |
| onReady | function | No | スクリプト準備完了コールバック（マウント毎に実行） | 関数型 |
| onError | function | No | スクリプト読み込みエラーコールバック | 関数型 |
| children | ReactNode | No | インラインスクリプト内容 | - |
| dangerouslySetInnerHTML | object | No | インラインスクリプトHTML | - |
| stylesheets | string[] | No | スクリプトに関連するスタイルシートURL | - |
| nonce | string | No | CSPノンス値 | - |

### 入力データソース

- コンポーネントProps経由でのユーザー指定値
- HeadManagerContext（Pages Router: updateScripts, scripts, getIsSsr, appDir, nonce）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| script要素（SSR） | React.ReactElement \| null | beforeInteractive時のSSR用script要素 |
| DOM script要素 | HTMLScriptElement | クライアントサイドで動的に作成されるscript要素 |

### 出力先

- beforeInteractive(App Router): SSR時にscript要素をReact Tree内にインライン生成
- beforeInteractive(Pages Router): HeadManagerContext経由でDocumentに注入
- afterInteractive/lazyOnload: document.bodyにscript要素を動的追加

## 処理フロー

### 処理シーケンス

```
1. Script コンポーネントマウント
   ├─ HeadManagerContext から updateScripts, appDir, nonce 取得
   └─ props.nonce || context.nonce で nonce 決定
2. onReady Effect（最初のマウント時）
   ├─ LoadCacheにキーが存在: onReady を即座実行
   └─ LoadCacheにキー不存在: スキップ（loadScript後に実行される）
3. loadScript Effect（最初のマウント時）
   ├─ afterInteractive: loadScript(props) を即座実行
   └─ lazyOnload: loadLazyScript(props) を実行
4. beforeInteractive / worker 戦略
   ├─ updateScripts あり: scripts配列に追加し updateScripts 呼び出し
   ├─ SSR中（getIsSsr === true）: LoadCache に追加のみ
   └─ SSR後（getIsSsr === false）: loadScript を直接実行
5. loadScript 関数
   ├─ LoadCache チェック（既読み込みならスキップ）
   ├─ ScriptCache チェック（同一srcの重複防止）
   ├─ script要素生成 + イベントリスナー設定
   ├─ setAttributesFromProps で属性設定
   ├─ worker戦略: type="text/partytown" 設定
   ├─ data-nscript 属性で戦略を記録
   ├─ stylesheets の読み込み
   └─ document.body.appendChild(el) で DOM 追加
6. App Router 固有処理
   ├─ ReactDOM.preload / ReactDOM.preinit
   └─ self.__next_s 配列によるSSRスクリプト注入
```

### フローチャート

```mermaid
flowchart TD
    A[Script コンポーネントマウント] --> B{strategy}
    B -->|beforeInteractive| C{appDir?}
    C -->|Yes| D[SSR script 要素生成]
    C -->|No| E{updateScripts?}
    E -->|Yes| F[scripts 配列に追加]
    E -->|No| G[loadScript 直接実行]
    D --> H[ReactDOM.preload]
    H --> I[self.__next_s 注入]
    B -->|afterInteractive| J[useEffect で loadScript]
    B -->|lazyOnload| K[useEffect で loadLazyScript]
    B -->|worker| L[type='text/partytown']
    J --> M[loadScript]
    K --> N{document.readyState?}
    N -->|complete| O[requestIdleCallback → loadScript]
    N -->|!complete| P[window.load → requestIdleCallback → loadScript]
    M --> Q{LoadCache/ScriptCache?}
    Q -->|cached| R[スキップ]
    Q -->|new| S[script要素生成]
    S --> T[document.body.appendChild]
    T --> U[onLoad / onReady コールバック]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-60-01 | 重複読み込み防止 | id または src をキーとしてLoadCache/ScriptCacheで重複を防止 | 常時 |
| BR-60-02 | onReady再実行 | コンポーネント再マウント時にLoadCacheにキーがあればonReadyを再実行 | 2回目以降のマウント |
| BR-60-03 | lazyOnload遅延 | lazyOnloadはwindow.loadイベント後にrequestIdleCallbackで実行 | strategy='lazyOnload' |
| BR-60-04 | Strict Mode対応 | hasOnReadyEffectCalled/hasLoadScriptEffectCalledで2重実行防止 | React Strict Mode |
| BR-60-05 | worker戦略 | type="text/partytown"を設定してPartytown経由で実行 | strategy='worker' |
| BR-60-06 | インラインスクリプト | dangerouslySetInnerHTMLまたはchildren指定時は即座afterLoad実行 | src未指定時 |
| BR-60-07 | beforeInteractive SSR | App Routerではself.__next_s配列にスクリプト情報をpush | appDir && beforeInteractive |

### 計算ロジック

- キャッシュキー: `id || src` で決定
- loadPromise: script要素のloadイベントをPromiseとしてラップし、ScriptCacheに格納

## データベース操作仕様

該当なし

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | スクリプトエラー | 外部スクリプトの読み込み失敗 | onErrorコールバック実行、loadPromise reject |
| - | ネットワークエラー | スクリプトURLへのアクセス失敗 | onErrorコールバック実行 |

### リトライ仕様

スクリプトの自動リトライは行わない。onErrorコールバック内でアプリケーション独自のリトライロジックを実装可能。

## トランザクション仕様

該当なし

## パフォーマンス要件

- beforeInteractive: ページのインタラクティブ前に実行（最高優先度）
- afterInteractive: ページのハイドレーション後に実行（通常優先度）
- lazyOnload: ページの完全読み込み後、アイドル時に実行（最低優先度）
- worker: Web Worker（Partytown）で実行（メインスレッドへの影響ゼロ）
- ReactDOM.preloadによる外部スクリプトの事前読み込み（App Router）

## セキュリティ考慮事項

- nonce属性によるContent Security Policy（CSP）対応
- data-nscript属性でNext.js管理のスクリプトを識別
- Subresource Integrity（integrity属性）のサポート

## 備考

- `__nextScript`プロパティがScript関数に設定されており、Next.js内部でのScript識別に使用
- `initScriptLoader`関数は外部からスクリプトを初期化する際に使用
- `handleClientScriptLoad`はクライアントサイドでのスクリプト読み込みを直接トリガー
- `addBeforeInteractiveToCache`はSSRで既に読み込まれたbeforeInteractiveスクリプトをLoadCacheに追加

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | script.tsx | `packages/next/src/client/script.tsx` | **10-11行目**: ScriptCache（Map）とLoadCache（Set）- 重複防止キャッシュ |
| 1-2 | script.tsx | `packages/next/src/client/script.tsx` | **13-21行目**: ScriptProps型定義 - strategy, onLoad, onReady, onError, stylesheets |

**読解のコツ**: ScriptCacheはsrcをキーとしてloadPromiseを格納し、同一URLの複数コンポーネントでPromiseを共有する。LoadCacheはid/srcをキーとして読み込み完了を追跡する。

#### Step 2: スクリプト読み込みの中核を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | script.tsx | `packages/next/src/client/script.tsx` | **61-155行目**: `loadScript`関数 - DOM script要素の生成と読み込み |

**主要処理フロー**:
1. **74-78行目**: LoadCacheチェック（既読み込みならreturn）
2. **82-88行目**: ScriptCacheチェック（同一srcのPromise共有）
3. **100-117行目**: script要素生成、load/errorイベントリスナー設定
4. **119-139行目**: インライン/外部スクリプトの分岐処理
5. **141行目**: `setAttributesFromProps`で属性設定
6. **143-145行目**: worker戦略時のtype設定
7. **147行目**: data-nscript属性設定
8. **150-152行目**: stylesheets読み込み
9. **154行目**: `document.body.appendChild(el)` でDOM追加

#### Step 3: Scriptコンポーネント本体を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | script.tsx | `packages/next/src/client/script.tsx` | **199-381行目**: `Script`関数コンポーネント本体 |

**主要処理フロー**:
1. **212-213行目**: HeadManagerContextからSSR/appDir情報取得
2. **244-256行目**: onReady useEffect - 再マウント時のonReady再実行
3. **260-270行目**: loadScript useEffect - afterInteractive/lazyOnload実行
4. **272-294行目**: beforeInteractive/worker戦略の処理
5. **298-377行目**: App Router固有処理（ReactDOM.preload, self.__next_s注入）

#### Step 4: ヘルパー関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | script.tsx | `packages/next/src/client/script.tsx` | **28-59行目**: `insertStylesheets` - ReactDOM.preinitまたはlink要素でCSS読み込み |
| 4-2 | script.tsx | `packages/next/src/client/script.tsx` | **157-166行目**: `handleClientScriptLoad` - lazyOnload分岐 |
| 4-3 | script.tsx | `packages/next/src/client/script.tsx` | **168-176行目**: `loadLazyScript` - readyState/loadイベント/requestIdleCallback |
| 4-4 | script.tsx | `packages/next/src/client/script.tsx` | **178-192行目**: `addBeforeInteractiveToCache`, `initScriptLoader` |

### プログラム呼び出し階層図

```
Script (関数コンポーネント)
    |
    +-- useContext(HeadManagerContext) ..... SSR/appDir 情報取得
    |
    +-- useEffect [onReady] ............... 再マウント時 onReady
    |
    +-- useEffect [loadScript] ............ afterInteractive/lazyOnload
    |       +-- loadScript() .............. DOM script 生成
    |       |       +-- setAttributesFromProps()
    |       |       +-- insertStylesheets()
    |       |       +-- document.body.appendChild()
    |       +-- loadLazyScript() .......... 遅延読み込み
    |               +-- requestIdleCallback()
    |               +-- loadScript()
    |
    +-- beforeInteractive/worker 処理
    |       +-- updateScripts() ........... Pages Router: Head 管理
    |       +-- loadScript() .............. SSR 後の直接読み込み
    |
    +-- App Router 固有
            +-- ReactDOM.preinit() ........ スタイルシート事前初期化
            +-- ReactDOM.preload() ........ スクリプト事前読み込み
            +-- self.__next_s.push() ...... SSR スクリプト注入
```

### データフロー図

```
[入力]                        [処理]                          [出力]

Props (src, strategy, ...) -> Script コンポーネント -------> script 要素 / null
HeadManagerContext ----------> |
                               |
strategy 判定 ----------------> loadScript / SSR 処理 ------> DOM script 要素
                               |
ScriptCache/LoadCache -------> 重複チェック -----------------> スキップ or 読み込み
                               |
stylesheets -----------------> insertStylesheets ------------> link 要素 / preinit
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| script.tsx | `packages/next/src/client/script.tsx` | ソース | Scriptコンポーネント本体（386行） |
| set-attributes-from-props.ts | `packages/next/src/client/set-attributes-from-props.ts` | ソース | DOM属性設定ユーティリティ |
| request-idle-callback.ts | `packages/next/src/client/request-idle-callback.ts` | ソース | requestIdleCallbackポリフィル |
| head-manager-context.shared-runtime.ts | `packages/next/src/shared/lib/head-manager-context.shared-runtime.ts` | ソース | HeadManagerContext定義 |
