B-159計画書: ツール・チートシートのダイナミックインポート廃止・静的化
B-159 実施計画書: ツール・チートシートのダイナミックインポート廃止・静的化
概要
next/dynamic によるダイナミックインポートを廃止し、静的インポートに切り替えることで、ローディングフラッシュ("Loading..." の一瞬表示)を解消する。チートシート(7個)とツール(33個)で異なるアプローチを採用する。
アプローチ
researcher の調査結果と実際のコードを精査した結果、推奨アプローチをそのまま採用する。
- チートシート(7個): アプローチ C -- CheatsheetRenderer.tsx を廃止し、page.tsx(サーバーコンポーネント)から直接レンダリング
- ツール(33個): アプローチ B -- ToolRenderer.tsx 内で next/dynamic を静的インポートマップに置き換え
アプローチ選定の根拠
- チートシートのコンポーネントは全てサーバーコンポーネント("use client" なし)であることを grep で確認済み。クライアントコンポーネント経由で動的読み込みする現状は根本的に不適切であり、サーバーコンポーネントとして直接レンダリングするのが正しい。
- ツールのコンポーネントは全て "use client" であるため、クライアントバンドルに含める必要がある。現状は全スラッグ分の dynamic() をループで初期化しているためコード分割の恩恵がなく、静的インポートマップに切り替えてもバンドルサイズへの実質的な悪影響はない。
- 個別ページ分割(アプローチ A)は 40 ファイル新規作成+レジストリパターン崩壊のため不採用。
変更対象ファイル一覧
変更するファイル(8ファイル)
| # | ファイル | 変更内容 |
|---|---|---|
| 1 | src/tools/types.ts |
ToolDefinition から componentImport フィールドを削除 |
| 2 | src/cheatsheets/types.ts |
CheatsheetDefinition から componentImport フィールドを削除 |
| 3 | src/tools/registry.ts |
全33エントリから componentImport 行を削除 |
| 4 | src/cheatsheets/registry.ts |
全7エントリから componentImport 行を削除 |
| 5 | src/app/tools/[slug]/ToolRenderer.tsx |
next/dynamic を廃止し、静的インポートマップに書き換え |
| 6 | src/app/cheatsheets/[slug]/page.tsx |
CheatsheetRenderer を廃止し、直接コンポーネントをレンダリング |
| 7 | src/cheatsheets/__tests__/registry.test.ts |
型変更に伴い必要ならテスト修正(現テストは componentImport を直接テストしていないため影響軽微) |
| 8 | docs/new-feature-guide.md |
ツール/チートシート追加手順を更新(componentImport 不要、ToolRenderer.tsx への追加が必要) |
削除するファイル(1ファイル)
| # | ファイル | 理由 |
|---|---|---|
| 1 | src/app/cheatsheets/[slug]/CheatsheetRenderer.tsx |
不要になるため完全削除 |
変更不要なファイル(確認済み)
src/app/tools/[slug]/opengraph-image.tsx-- toolsBySlug の meta のみ参照、componentImport 未使用src/app/cheatsheets/[slug]/opengraph-image.tsx-- cheatsheetsBySlug の meta のみ参照、componentImport 未使用src/tools/*/logic.test.ts(33個)-- ロジックテストのみ、レンダリングに無関係src/tools/_components/ToolLayout.tsx-- レイアウトコンポーネント、レンダリング方式に無関係src/tools/_components/ErrorBoundary.tsx-- ToolRenderer.tsx 内で引き続き使用src/blog/content/2026-02-14-nextjs-static-tool-pages-design-pattern.md-- 過去記事、componentImport への言及はあるが歴史的記録として変更不要
各ファイルの変更内容の詳細
1. src/tools/types.ts
変更: ToolDefinition インターフェースから componentImport フィールドを削除する。
変更前:
export interface ToolDefinition {
meta: ToolMeta;
componentImport: () => Promise<{ default: React.ComponentType }>;
}
変更後:
export interface ToolDefinition {
meta: ToolMeta;
}
2. src/cheatsheets/types.ts
変更: CheatsheetDefinition インターフェースから componentImport フィールドを削除する。
変更前:
export interface CheatsheetDefinition {
meta: CheatsheetMeta;
componentImport: () => Promise<{ default: React.ComponentType }>;
}
変更後:
export interface CheatsheetDefinition {
meta: CheatsheetMeta;
}
3. src/tools/registry.ts
変更: toolEntries 配列の全33エントリから componentImport 行を削除する。メタデータインポートと toolsBySlug / allToolMetas / getAllToolSlugs はそのまま維持。
変更前(各エントリ):
{
meta: charCountMeta,
componentImport: () => import("./char-count/Component"),
},
変更後(各エントリ):
{
meta: charCountMeta,
},
4. src/cheatsheets/registry.ts
変更: cheatsheetEntries 配列の全7エントリから componentImport 行を削除する。
変更前(各エントリ):
{
meta: regexMeta,
componentImport: () => import("./regex/Component"),
},
変更後(各エントリ):
{
meta: regexMeta,
},
5. src/app/tools/[slug]/ToolRenderer.tsx
変更: next/dynamic を廃止し、全33個のツールコンポーネントを静的インポートで読み込む。slug をキーとした Record で参照する。
変更のポイント:
import dynamic from "next/dynamic"を削除import { toolsBySlug } from "@/tools/registry"を削除(registry はもう不要)- 33個のコンポーネントを個別に静的インポート
const componentsBySlug: Record<string, React.ComponentType>を定義- ToolErrorBoundary は引き続き使用
loading: () => <div>Loading...</div>が消えることでローディングフラッシュ解消
変更後のイメージ:
"use client";
import ToolErrorBoundary from "@/tools/_components/ErrorBoundary";
import CharCountComponent from "@/tools/char-count/Component";
import JsonFormatterComponent from "@/tools/json-formatter/Component";
// ... 他31個のインポート ...
const componentsBySlug: Record<string, React.ComponentType> = {
"char-count": CharCountComponent,
"json-formatter": JsonFormatterComponent,
// ... 他31個のマッピング ...
};
interface ToolRendererProps {
slug: string;
}
export default function ToolRenderer({ slug }: ToolRendererProps) {
const ToolComponent = componentsBySlug[slug];
if (!ToolComponent) return null;
return (
<ToolErrorBoundary>
<ToolComponent />
</ToolErrorBoundary>
);
}
注意: 33個のインポートとマッピングは行数が多くなるが、明示的で型安全であり、ベストプラクティスに沿っている。
6. src/app/cheatsheets/[slug]/page.tsx
変更: CheatsheetRenderer を廃止し、page.tsx 内にチートシートコンポーネントのマッピングを直接記述する。page.tsx はサーバーコンポーネントであり、チートシートコンポーネントもサーバーコンポーネントであるため、完全にサーバーサイドでレンダリングできる。
変更のポイント:
import CheatsheetRenderer from "./CheatsheetRenderer"を削除- 7個のチートシートコンポーネントを静的インポート
const cheatsheetComponentsBySlug: Record<string, React.ComponentType>を定義<CheatsheetRenderer slug={slug} />をcheatsheetComponentsBySlug[slug]の直接レンダリングに変更
変更後のイメージ:
import { notFound } from "next/navigation";
import {
cheatsheetsBySlug,
getAllCheatsheetSlugs,
} from "@/cheatsheets/registry";
import {
generateCheatsheetMetadata,
generateCheatsheetJsonLd,
safeJsonLdStringify,
} from "@/lib/seo";
import CheatsheetLayout from "@/cheatsheets/_components/CheatsheetLayout";
import RegexComponent from "@/cheatsheets/regex/Component";
import GitComponent from "@/cheatsheets/git/Component";
import MarkdownComponent from "@/cheatsheets/markdown/Component";
import HttpStatusCodesComponent from "@/cheatsheets/http-status-codes/Component";
import CronComponent from "@/cheatsheets/cron/Component";
import HtmlTagsComponent from "@/cheatsheets/html-tags/Component";
import SqlComponent from "@/cheatsheets/sql/Component";
const cheatsheetComponentsBySlug: Record<string, React.ComponentType> = {
regex: RegexComponent,
git: GitComponent,
markdown: MarkdownComponent,
"http-status-codes": HttpStatusCodesComponent,
cron: CronComponent,
"html-tags": HtmlTagsComponent,
sql: SqlComponent,
};
// generateStaticParams, generateMetadata はそのまま維持
export default async function CheatsheetPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const cheatsheet = cheatsheetsBySlug.get(slug);
if (!cheatsheet) notFound();
const CheatsheetComponent = cheatsheetComponentsBySlug[slug];
if (!CheatsheetComponent) notFound();
return (
<CheatsheetLayout meta={cheatsheet.meta}>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: safeJsonLdStringify(
generateCheatsheetJsonLd(cheatsheet.meta),
),
}}
/>
<CheatsheetComponent />
</CheatsheetLayout>
);
}
7. src/app/cheatsheets/[slug]/CheatsheetRenderer.tsx
変更: ファイルを完全削除する。
8. src/cheatsheets/tests/registry.test.ts
変更: 現テストは componentImport を直接テストしていないため、型定義変更後もそのまま通る可能性が高い。ただし念のためビルド・テスト実行で確認する。型エラーが発生した場合のみ修正。
9. docs/new-feature-guide.md
変更: 「4. 新しいツール追加の手順」と「チートシート追加の手順」(現在はチートシートの手順が明記されていないが、ツールと同様のパターンが想定される)を更新する。
ツール追加手順の変更:
- 「4-3. registry.ts にツールを登録」から componentImport の例を削除
- 「4-3b. ToolRenderer.tsx にコンポーネントを追加」を新規追加(静的インポートとマッピングの追加が必要)
チートシート追加手順の追加:
- registry.ts に meta を登録
- page.tsx にコンポーネントのインポートとマッピングを追加
作業手順
フェーズ 1: 型定義とレジストリの変更
src/tools/types.tsから componentImport を削除src/cheatsheets/types.tsから componentImport を削除src/tools/registry.tsの全33エントリから componentImport を削除src/cheatsheets/registry.tsの全7エントリから componentImport を削除
フェーズ 2: チートシートの静的化
src/app/cheatsheets/[slug]/page.tsxを書き換え(7個のコンポーネントを静的インポート、直接レンダリング)src/app/cheatsheets/[slug]/CheatsheetRenderer.tsxを削除
フェーズ 3: ツールの静的化
src/app/tools/[slug]/ToolRenderer.tsxを書き換え(33個のコンポーネントを静的インポート、マッピング)
フェーズ 4: ドキュメント更新
docs/new-feature-guide.mdを更新
フェーズ 5: 検証
npm run typecheck-- 型エラーがないことを確認npm run test-- 既存テストが全て通ることを確認npm run lint-- lint エラーがないことを確認npm run build-- ビルドが成功することを確認npm run devでローカル確認 -- ツールページ・チートシートページでローディングフラッシュが発生しないことを目視確認
テストへの影響と対応
影響なし(確認済み)
src/tools/*/logic.test.ts(33個): 純粋関数のテスト。componentImport に依存しない。src/tools/_components/__tests__/ToolLayout.test.tsx: レイアウトのテスト。レンダリング方式に依存しない。src/cheatsheets/_components/__tests__/: CheatsheetLayout, CodeBlock のテスト。レンダリング方式に依存しない。
影響が軽微
src/cheatsheets/__tests__/registry.test.ts: 現テストは componentImport を直接テストしていない。cheatsheetsBySlug.size や allCheatsheetMetas のみを検証しているため、型定義変更後も通るはず。念のため確認。
新規テストの検討
componentsBySlug マップ内の全スラッグが registry の全スラッグと一致していることを検証するテストを追加することを推奨する。registry.ts にツールを登録したが ToolRenderer.tsx のマッピングへの追加を忘れた場合に検出できる。ただしこれは B-159 の必須スコープではなく、レビュー時に判断する。
新しいツール/チートシート追加時の手順変更
ツール追加(変更後)
src/tools/{slug}/にディレクトリ作成(Component.tsx, meta.ts, logic.ts)-- 変更なしsrc/tools/registry.tsに meta を登録 -- 変更: componentImport は不要になる- 新規:
src/app/tools/[slug]/ToolRenderer.tsxにインポートとマッピングを追加
チートシート追加(変更後)
src/cheatsheets/{slug}/にディレクトリ作成(Component.tsx, meta.ts)-- 変更なしsrc/cheatsheets/registry.tsに meta を登録 -- 変更: componentImport は不要になる- 新規:
src/app/cheatsheets/[slug]/page.tsxにインポートとマッピングを追加
DX への影響
- 登録箇所が 1箇所(registry.ts)から 2箇所(registry.ts + Renderer/page.tsx)に増える
- ただし、追加忘れの場合はビルド時またはランタイムで即座に検出可能(コンポーネントが null になる)
- 前述の「マップ一致テスト」を導入すれば CI でも検出可能
ビルドサイズへの影響
- チートシート: クライアントバンドルから CheatsheetRenderer.tsx(+ 7個のチートシートコンポーネント)が除外される。チートシートコンポーネントは元々サーバーコンポーネントであるため、正しくサーバーサイドのみでレンダリングされるようになる。バンドルサイズは減少する。
- ツール: 現状でもループ初期化により全33コンポーネントがバンドルに含まれている可能性が高いため、静的インポートに変更してもバンドルサイズへの実質的な悪影響はほぼない。next/dynamic のランタイムオーバーヘッド(React.lazy + Suspense 関連コード)が削減されるため、むしろ微減する可能性がある。
リスクと対策
| リスク | 影響 | 対策 |
|---|---|---|
| ツール追加時に ToolRenderer.tsx への追加を忘れる | 該当ツールが表示されない | ドキュメント更新 + マップ一致テスト追加を検討 |
| 一部ツールコンポーネントのインポートでビルドエラー | ビルド失敗 | ビルド検証ステップで検出 |
| 静的インポートによるバンドルサイズ増加 | パフォーマンス低下 | 前述の通り実質的な増加はないと判断。ビルド出力サイズを変更前後で比較して確認 |
作業の分割
この作業は 1つのビルダータスクとして実施可能。フェーズ 1-4 は全て連動しており、分割すると中間状態で型エラーやビルドエラーが発生するため、1 回のビルダー実行で完了させるのが適切。
作業完了後、レビュアーに以下の観点でレビューを依頼する:
- 全ツール(33個)・全チートシート(7個)のスラッグがマッピングに漏れなく含まれているか
- typecheck, test, lint, build が全て通るか
- ローディングフラッシュが解消されているか(dev サーバーでの目視確認は不要だが、コード上で Loading... フォールバックが完全に除去されていることを確認)
- ドキュメントが正しく更新されているか