
Lazy Loading Patterns
- 15 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Helps with ai & agent building tasks.
About
lazy-loading-patterns is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- lazy-loading-patterns
- AI & Agent Building
- AI-coding skill
Lazy Loading Patterns by the numbers
- 15 all-time installs (skills.sh)
- Ranked #11,187 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill lazy-loading-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
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)
import { lazy, Suspense } from 'react';
// Lazy load component - code split at this boundary
const HeavyEditor = lazy(() => import('./HeavyEditor'));
function EditorPage() {
return (
<Suspense fallback={<EditorSkeleton />}>
<HeavyEditor />
</Suspense>
);
}
// With named exports (requires intermediate module)
const Chart = lazy(() =>
import('./charts').then(module => ({ default: module.LineChart }))
);2. React 19 use() Hook (Modern Pattern)
import { use, Suspense } from 'react';
// Create promise outside component
const dataPromise = fetchData();
function DataDisplay() {
// Suspense-aware promise unwrapping
const data = use(dataPromise);
return <div>{data.title}</div>;
}
// Usage with Suspense
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>3. Route-Based Code Splitting (React Router 7.x)
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: <Layout />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
{ path: 'analytics', element: <Analytics /> },
],
},
]);
// Root with Suspense boundary
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<RouterProvider router={router} />
</Suspense>
);
}4. Intersection Observer Lazy Loading
import { useRef, useState, useEffect, lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function LazyOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(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 (
<div ref={ref}>
{isVisible ? children : <Placeholder />}
</div>
);
}
// Usage
<LazyOnScroll>
<Suspense fallback={<ChartSkeleton />}>
<HeavyComponent />
</Suspense>
</LazyOnScroll>5. Prefetching on Hover/Focus
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 (
<Link
to={to}
onMouseEnter={prefetchRoute}
onFocus={prefetchRoute}
preload="intent" // React Router preloading
>
{children}
</Link>
);
}6. Module Preload Hints
<!-- In index.html or via helmet -->
<link rel="modulepreload" href="/assets/dashboard-chunk.js" />
<link rel="modulepreload" href="/assets/vendor-react.js" />
<!-- Prefetch for likely next navigation -->
<link rel="prefetch" href="/assets/settings-chunk.js" />// Programmatic preloading
function preloadComponent(importFn: () => Promise<any>) {
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
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 (
<Suspense fallback={<DashboardSkeleton />}>
{useNewDashboard ? <NewDashboard /> : <LegacyDashboard />}
</Suspense>
);
}Suspense Boundaries Strategy
// ✅ CORRECT: Granular Suspense boundaries
function Dashboard() {
return (
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<UsersChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
// ❌ WRONG: Single boundary blocks entire UI
function Dashboard() {
return (
<Suspense fallback={<FullPageSkeleton />}>
<RevenueChart />
<UsersChart />
<RecentOrders />
</Suspense>
);
}Error Boundaries with Lazy Components
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
<LazyErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Skeleton />}>
<LazyComponent />
</Suspense>
</LazyErrorBoundary>Bundle Analysis Integration
// 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
// 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)
// ❌ 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 <Chart />; // Will throw!
}
// ❌ NEVER: Lazy inside render (creates new component each render)
function App() {
const Component = lazy(() => import('./Component')); // ❌
return <Component />;
}
// ❌ 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
<Suspense fallback={<Skeleton />}>
<LazyComponent /> {/* What if import fails? */}
</Suspense>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 loadingvite-advanced- Vite code splitting configurationrender-optimization- React render performancereact-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 patternsreferences/intersection-observer.md- Scroll-triggered lazy loadingscripts/lazy-component.tsx- Lazy component template
Route-Based Code Splitting
React Router 7.x Lazy Routes
import { lazy } from 'react';
import { createBrowserRouter } from 'react-router';
// Define lazy routes
const routes = [
{
path: '/',
lazy: () => import('./pages/Home'),
},
{
path: '/dashboard',
lazy: () => import('./pages/Dashboard'),
children: [
{
path: 'analytics',
lazy: () => import('./pages/Analytics'),
},
{
path: 'settings',
lazy: () => import('./pages/Settings'),
},
],
},
];
const router = createBrowserRouter(routes);Vite Manual Chunks
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunks
'react-vendor': ['react', 'react-dom', 'react-router'],
'query-vendor': ['@tanstack/react-query'],
// Feature chunks (match route structure)
'dashboard': [
'./src/pages/Dashboard',
'./src/pages/Analytics',
],
'settings': [
'./src/pages/Settings',
'./src/pages/Profile',
],
},
},
},
},
});Prefetch on Route Hover
import { useQueryClient } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router';
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const queryClient = useQueryClient();
const prefetch = () => {
// Prefetch route data
queryClient.prefetchQuery({
queryKey: ['route', to],
queryFn: () => fetchRouteData(to),
});
};
return (
<Link
to={to}
onMouseEnter={prefetch}
onFocus={prefetch}
preload="intent"
>
{children}
</Link>
);
}Bundle Size Monitoring
# After build, check chunk sizes
npx vite build
# Output shows chunk sizes
# For detailed analysis
npx vite-bundle-visualizerimport { lazy, Suspense, ComponentType, ReactNode, ComponentProps } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { motion } from 'motion/react';
import { pulse } from '@/lib/animations';
/**
* Create a lazy-loaded component with Suspense boundary
* @param importFn - Dynamic import function
* @param fallback - Optional custom fallback component
*/
export function createLazyComponent<T extends ComponentType<unknown>>(
importFn: () => Promise<{ default: T }>,
fallback?: ReactNode
) {
const LazyComponent = lazy(importFn);
return function LazyWrapper(props: ComponentProps<T>) {
return (
<Suspense fallback={fallback || <DefaultSkeleton />}>
<LazyComponent {...props} />
</Suspense>
);
};
}
/**
* Default skeleton loading component
*/
function DefaultSkeleton() {
return (
<motion.div
variants={pulse}
initial="initial"
animate="animate"
className="h-32 w-full rounded-lg bg-muted"
aria-label="Loading..."
/>
);
}
/**
* Lazy component with error boundary
*/
export function LazyWithErrorBoundary<T extends ComponentType<unknown>>({
importFn,
fallback,
errorFallback,
...props
}: {
importFn: () => Promise<{ default: T }>;
fallback?: ReactNode;
errorFallback?: ReactNode;
} & ComponentProps<T>) {
const LazyComponent = lazy(importFn);
return (
<ErrorBoundary fallback={errorFallback || <ErrorFallback />}>
<Suspense fallback={fallback || <DefaultSkeleton />}>
<LazyComponent {...props} />
</Suspense>
</ErrorBoundary>
);
}
function ErrorFallback() {
return (
<div className="rounded-lg border border-destructive bg-destructive/10 p-4">
<p className="text-sm text-destructive">Failed to load component</p>
</div>
);
}
// Example usage:
// const LazyChart = createLazyComponent(() => import('./Chart'), <ChartSkeleton />);
// <LazyChart data={data} />