--- name: lazy-loading-patterns description: Code splitting and lazy loading with React.lazy, Suspense, route-based splitting, intersection observer, and preload strategies for optimal bundle performance. Use when implementing lazy loading or preloading. tags: [lazy-loading, code-splitting, suspense, dynamic-import, intersection-observer, preload, react-19, performance] context: fork agent: frontend-ui-developer version: 1.0.0 author: OrchestKit user-invocable: false --- # Lazy Loading Patterns Code splitting and lazy loading patterns for React 19 applications using `React.lazy`, `Suspense`, route-based splitting, and intersection observer strategies. ## Overview - Reducing initial bundle size for faster page loads - Route-based code splitting in SPAs - Lazy loading heavy components (charts, editors, modals) - Below-the-fold content loading - Conditional feature loading based on user permissions - Progressive image and media loading ## Core Patterns ### 1. React.lazy + Suspense (Standard Pattern) ```tsx import { lazy, Suspense } from 'react'; // Lazy load component - code split at this boundary const HeavyEditor = lazy(() => import('./HeavyEditor')); function EditorPage() { return ( }> ); } // With named exports (requires intermediate module) const Chart = lazy(() => import('./charts').then(module => ({ default: module.LineChart })) ); ``` ### 2. React 19 `use()` Hook (Modern Pattern) ```tsx import { use, Suspense } from 'react'; // Create promise outside component const dataPromise = fetchData(); function DataDisplay() { // Suspense-aware promise unwrapping const data = use(dataPromise); return
{data.title}
; } // Usage with Suspense }> ``` ### 3. Route-Based Code Splitting (React Router 7.x) ```tsx import { lazy } from 'react'; import { createBrowserRouter, RouterProvider } from 'react-router'; // Lazy load route components const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); const Analytics = lazy(() => import('./pages/Analytics')); const router = createBrowserRouter([ { path: '/', element: , children: [ { path: 'dashboard', element: }, { path: 'settings', element: }, { path: 'analytics', element: }, ], }, ]); // Root with Suspense boundary function App() { return ( }> ); } ``` ### 4. Intersection Observer Lazy Loading ```tsx import { useRef, useState, useEffect, lazy, Suspense } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function LazyOnScroll({ children }: { children: React.ReactNode }) { const ref = useRef(null); const [isVisible, setIsVisible] = useState(false); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { rootMargin: '100px' } // Load 100px before visible ); if (ref.current) observer.observe(ref.current); return () => observer.disconnect(); }, []); return (
{isVisible ? children : }
); } // Usage }> ``` ### 5. Prefetching on Hover/Focus ```tsx import { useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router'; function NavLink({ to, children }: { to: string; children: React.ReactNode }) { const queryClient = useQueryClient(); const prefetchRoute = () => { // Prefetch data for the route queryClient.prefetchQuery({ queryKey: ['page', to], queryFn: () => fetchPageData(to), }); // Prefetch the component chunk import(`./pages/${to}`); }; return ( {children} ); } ``` ### 6. Module Preload Hints ```html ``` ```tsx // Programmatic preloading function preloadComponent(importFn: () => Promise) { const link = document.createElement('link'); link.rel = 'modulepreload'; link.href = importFn.toString().match(/import\("(.+?)"\)/)?.[1] || ''; document.head.appendChild(link); } ``` ### 7. Conditional Loading with Feature Flags ```tsx import { lazy, Suspense } from 'react'; import { useFeatureFlag } from '@/hooks/useFeatureFlag'; const NewDashboard = lazy(() => import('./NewDashboard')); const LegacyDashboard = lazy(() => import('./LegacyDashboard')); function Dashboard() { const useNewDashboard = useFeatureFlag('new-dashboard'); return ( }> {useNewDashboard ? : } ); } ``` ## Suspense Boundaries Strategy ```tsx // ✅ CORRECT: Granular Suspense boundaries function Dashboard() { return (
}> }> }>
); } // ❌ WRONG: Single boundary blocks entire UI function Dashboard() { return ( }> ); } ``` ## Error Boundaries with Lazy Components ```tsx import { Component, ErrorInfo, ReactNode } from 'react'; class LazyErrorBoundary extends Component< { children: ReactNode; fallback: ReactNode }, { hasError: boolean } > { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Lazy load failed:', error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } // Usage }> }> ``` ## Bundle Analysis Integration ```typescript // vite.config.ts import { defineConfig } from 'vite'; import { visualizer } from 'rollup-plugin-visualizer'; export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { // Vendor splitting 'vendor-react': ['react', 'react-dom'], 'vendor-router': ['react-router'], 'vendor-query': ['@tanstack/react-query'], // Feature splitting 'feature-charts': ['recharts', 'd3'], 'feature-editor': ['@tiptap/react', '@tiptap/starter-kit'], }, }, }, }, plugins: [ visualizer({ filename: 'dist/bundle-analysis.html', open: true, gzipSize: true, }), ], }); ``` ## Performance Budgets ```json // package.json { "bundlesize": [ { "path": "dist/assets/index-*.js", "maxSize": "80kb" }, { "path": "dist/assets/vendor-react-*.js", "maxSize": "50kb" }, { "path": "dist/assets/feature-*-*.js", "maxSize": "100kb" } ] } ``` ## Anti-Patterns (FORBIDDEN) ```tsx // ❌ NEVER: Lazy load small components (< 5KB) const Button = lazy(() => import('./Button')); // Overhead > savings // ❌ NEVER: Missing Suspense boundary function App() { const Chart = lazy(() => import('./Chart')); return ; // Will throw! } // ❌ NEVER: Lazy inside render (creates new component each render) function App() { const Component = lazy(() => import('./Component')); // ❌ return ; } // ❌ NEVER: Lazy loading critical above-fold content const Hero = lazy(() => import('./Hero')); // Delays LCP! // ❌ NEVER: Over-splitting (too many small chunks) // Each chunk = 1 HTTP request = latency overhead // ❌ NEVER: Missing error boundary for network failures }> {/* What if import fails? */} ``` ## Key Decisions | Decision | Option A | Option B | Recommendation | |----------|----------|----------|----------------| | Splitting granularity | Per-component | Per-route | **Per-route** for most apps, per-component for heavy widgets | | Prefetch strategy | On hover | On viewport | **On hover** for nav links, **viewport** for content | | Suspense placement | Single root | Granular | **Granular** for independent loading | | Skeleton vs spinner | Skeleton | Spinner | **Skeleton** for content, spinner for actions | | Chunk naming | Auto-generated | Manual | **Manual** for debugging, auto for production | ## Related Skills - `core-web-vitals` - LCP optimization through lazy loading - `vite-advanced` - Vite code splitting configuration - `render-optimization` - React render performance - `react-server-components-framework` - Server-side code splitting ## Capability Details ### component-lazy-loading **Keywords**: React.lazy, dynamic import, Suspense, code splitting **Solves**: How to lazy load React components, reduce bundle size ### route-splitting **Keywords**: route, code splitting, React Router, lazy routes **Solves**: Route-based code splitting, per-page bundles ### intersection-observer **Keywords**: scroll, viewport, lazy, IntersectionObserver, below-fold **Solves**: Load components when scrolled into view ### suspense-patterns **Keywords**: Suspense, fallback, boundary, skeleton, loading **Solves**: Proper Suspense boundary placement, skeleton loading ### preloading **Keywords**: prefetch, preload, modulepreload, hover, intent **Solves**: Preload on hover, prefetch likely navigation ### bundle-optimization **Keywords**: bundle, chunks, splitting, manualChunks, vendor **Solves**: Optimize bundle splitting strategy, vendor chunks ## References - `references/route-splitting.md` - Route-based code splitting patterns - `references/intersection-observer.md` - Scroll-triggered lazy loading - `scripts/lazy-component.tsx` - Lazy component template