lazy-loading について
lazy-loading は、必要になるまでデータやページコンテンツの読み込みを遅らせる開発スキルです。操作、スクロール、ルート、ページネーションに応じた読み込みパターンを示します。
主な機能
- 01操作時のデータ読み込み
- 02表示領域に入ったセクションの読み込み
- 03ルート単位でのコード読み込み延期
- 04ページネーションや無限スクロール
- 05先読み過多の兆候の特定
活用シーン
- 01多くのデータを扱うダッシュボードの高速化
- 02非表示タブへの不要なリクエストの回避
- 03大きなリストの段階的な表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill lazy-loadingインストール後、Claude Code で/lazy-loadingを入力して読み込まれたか確認します。
詳細
遅延読み込み
概要
必要になったときにデータを読み込み、前もって読み込まないでください。使わないかもしれないデータを取得してはいけません。
最初にデータを読み込むと、帯域幅、メモリ、時間が無駄になります。必要に応じて読み込むことで、パフォーマンスとユーザーエクスペリエンスが向上します。
使用する場面
- ダッシュボードがすべてのデータを一度に読み込む
- ページが非表示のタブのデータを取得する
- 「念のため」のデータを読み込む
- 初回読み込みが遅い
絶対ルール
NEVER load data until it's actually needed.
例外はありません:
- 「必要になるかもしれない」から
- 「並列処理のほうが速い」から
- 「すべてを読み込むほうが簡単」だから
- 「それほどデータ量が多くない」から
検出:先行読み込み過多の兆候
最初にすべてを読み込んでいる場合は、いったん止めてください:
// ❌ VIOLATION: Load everything upfront
async function loadDashboard(userId: string) {
const [
profile,
preferences,
notifications, // User might not check
recentActivity, // Collapsed by default
analytics, // Expensive, rarely viewed
recommendations, // Below the fold
fullHistory // Paginated anyway
] = await Promise.all([
fetchProfile(userId),
fetchPreferences(userId),
fetchNotifications(userId),
fetchRecentActivity(userId),
fetchAnalytics(userId), // Takes 2 seconds!
fetchRecommendations(userId),
fetchFullHistory(userId) // 10MB of data!
]);
return { profile, preferences, notifications, ... };
}
問題点:
- 最も遅いデータ取得がすべてをブロックする
- 使われないデータにリソースを浪費する
- 体感パフォーマンスが悪い
正しいパターン:必要に応じて読み込む
// ✅ CORRECT: Load critical data first, rest on demand
// Initial load - only what's immediately visible
async function loadDashboard(userId: string) {
const [profile, preferences] = await Promise.all([
fetchProfile(userId),
fetchPreferences(userId)
]);
return { profile, preferences };
}
// React component with lazy loading
function Dashboard({ userId }) {
// Critical data loaded immediately
const { profile, preferences } = useInitialData(userId);
// Notifications: load when header mounts
const notifications = useLazyQuery(
() => fetchNotifications(userId),
{ loadOn: 'mount' }
);
// Analytics: load when tab is selected
const [analyticsTab, setAnalyticsTab] = useState(false);
const analytics = useLazyQuery(
() => fetchAnalytics(userId),
{ loadOn: analyticsTab }
);
// History: load when scrolled into view
const historyRef = useRef();
const history = useLazyQuery(
() => fetchHistory(userId),
{ loadOn: useIntersectionObserver(historyRef) }
);
return (
<div>
<Header profile={profile} notifications={notifications} />
<Tabs>
<Tab label="Overview">...</Tab>
<Tab label="Analytics" onSelect={() => setAnalyticsTab(true)}>
{analytics.loading ? <Skeleton /> : <AnalyticsChart data={analytics.data} />}
</Tab>
</Tabs>
<div ref={historyRef}>
{history.data && <HistoryList items={history.data} />}
</div>
</div>
);
}
遅延読み込みの手法
1. 操作時に読み込む
// Load when user clicks tab
const [showDetails, setShowDetails] = useState(false);
const details = useQuery(fetchDetails, { enabled: showDetails });
<Tab onClick={() => setShowDetails(true)}>
{details.data ?? <Skeleton />}
</Tab>
2. スクロール時に読み込む(Intersection Observer)
function LazySection({ loadFn, children }) {
const ref = useRef();
const [loaded, setLoaded] = useState(false);
const data = useQuery(loadFn, { enabled: loaded });
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => entry.isIntersecting && setLoaded(true)
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return <div ref={ref}>{data.data ? children(data.data) : <Skeleton />}</div>;
}
3. ルートごとに読み込む
// Next.js / React Router
const AnalyticsPage = lazy(() => import('./AnalyticsPage'));
<Route path="/analytics" element={
<Suspense fallback={<Loading />}>
<AnalyticsPage />
</Suspense>
} />
4. ページネーション/無限スクロール
function OrderList() {
const [page, setPage] = useState(1);
const { data, hasMore } = useOrders({ page, limit: 20 });
return (
<>
{data.map(order => <OrderRow key={order.id} order={order} />)}
{hasMore && <button onClick={() => setPage(p => p + 1)}>Load More</button>}
</>
);
}
圧力への対処プロトコル
1. 「必要になるかもしれない」
圧力:「ユーザーが必要とする場合に備えて、今読み込んでおこう」
回答:「かもしれない」ということは、おそらく必要ありません。実際に必要になったときに読み込みます。
**対応:**憶測ではなく、必要に応じて読み込みます。
2. 「並列処理のほうが速い」
圧力:「すべてを並列に読み込むほうが、順番に読み込むより速い」
**回答:**不要なデータを並列に読み込むより、読み込まないほうが速いです。
**対応:**必要なものは並列化します。必要になるかもしれないものは遅延読み込みします。
3. 「すべてを読み込むほうが簡単」
圧力:「取得関数を1つにまとめるほうが簡単」
**回答:**遅くて無駄の多いシンプルなコードは、シンプルとは言えません。
**対応:**構造化された遅延読み込みは、保守しやすく高パフォーマンスです。
危険信号 ― いったん止めて再検討してください
- ページ読み込み時に、5件以上の取得を含む
Promise.all - 折りたたまれたセクションや非表示のセクションのデータを取得している
- ページネーションせず、リスト全体を読み込んでいる
- 初回のページ読み込みが遅い
- 「読み込み中...」が1~2秒以上続く
これらはすべて、遅延読み込みを実装すべきということです。
クイックリファレンス
| 先行読み込み(通常は不適切) | 遅延読み込み(通常は適切) |
|---|---|
| マウント時にすべて読み込む | まず表示されているコンテンツを読み込む |
| 非表示のタブのデータを取得する | タブが選択されたときに取得する |
| リスト全体を一度に読み込む | ページネーション/無限スクロール |
| 画面下部のコンテンツ | Intersection Observer |
よくある正当化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「必要になるかもしれない」 | 必要になったときに読み込みます。 |
| 「並列処理のほうが速い」 | 最も速いのは、読み込まないことです。 |
| 「簡単だから」 | 遅いものはシンプルではありません。 |
| 「データ量は多くない」 | 積み重なれば大きくなります。帯域幅にはコストがかかります。 |
| 「準備しておくほうがUXがよい」 | 読み込みが遅いほうがUXは悪くなります。 |
要点
表示されているものを読み込み、残りは後回しにします。大きなリストはページネーションします。
初回読み込みでは、重要なデータだけを読み込みます。それ以外は、操作、スクロール、または画面遷移に応じて読み込みます。ユーザーが目にしないデータのために、待たせてはいけません。