PatternsCode Splitting Strategies
Component-Based Splitting
Dynamically import heavy components only when needed
Component-Based Splitting
Split heavy components that aren't always needed. Load them only when the user actually needs them.
🎯 When to Split Components
Split if component:
✅ > 50 KB (e.g., Rich Text Editor, Chart Library)
✅ Only used conditionally (Modals, Tabs)
✅ Below the fold (User may never scroll)
✅ Behind interaction (Click to show)
✅ Third-party library (PDF viewer, Video player)
Don't split:
❌ < 10 KB components
❌ Always visible (Header, Footer)
❌ Critical path (First render)📦 Heavy Component Examples
Rich Text Editor (500 KB!)
// ❌ BAD: Always loaded (even if never used)
import RichTextEditor from 'react-quill';
import 'react-quill/dist/quill.snow.css';
function BlogPost() {
const [editing, setEditing] = useState(false);
return (
<div>
<button onClick={() => setEditing(true)}>Edit</button>
{editing && <RichTextEditor value={content} />}
</div>
);
}
// ✅ GOOD: Load only when editing
const RichTextEditor = lazy(() => import('react-quill'));
function BlogPost() {
const [editing, setEditing] = useState(false);
return (
<div>
<button onClick={() => setEditing(true)}>Edit</button>
{editing && (
<Suspense fallback={<div>Loading editor...</div>}>
<RichTextEditor value={content} />
</Suspense>
)}
</div>
);
}Chart Library (300 KB!)
// ✅ GOOD: Split chart components
const LineChart = lazy(() => import('./charts/LineChart'));
const PieChart = lazy(() => import('./charts/PieChart'));
const BarChart = lazy(() => import('./charts/BarChart'));
function Dashboard() {
const [chartType, setChartType] = useState<'line' | 'pie' | 'bar'>('line');
const ChartComponent = {
line: LineChart,
pie: PieChart,
bar: BarChart
}[chartType];
return (
<div>
<select onChange={(e) => setChartType(e.target.value)}>
<option value="line">Line Chart</option>
<option value="pie">Pie Chart</option>
<option value="bar">Bar Chart</option>
</select>
<Suspense fallback={<ChartSkeleton />}>
<ChartComponent data={data} />
</Suspense>
</div>
);
}PDF Viewer (400 KB!)
const PDFViewer = lazy(() => import('react-pdf'));
function DocumentViewer({ url }: { url: string }) {
const [showPDF, setShowPDF] = useState(false);
return (
<div>
{!showPDF ? (
<button onClick={() => setShowPDF(true)}>
View PDF
</button>
) : (
<Suspense fallback={<div>Loading PDF viewer...</div>}>
<PDFViewer file={url} />
</Suspense>
)}
</div>
);
}🎨 Modal/Dialog Pattern
const UserProfileModal = lazy(() => import('./modals/UserProfileModal'));
const SettingsModal = lazy(() => import('./modals/SettingsModal'));
const ConfirmModal = lazy(() => import('./modals/ConfirmModal'));
function App() {
const [activeModal, setActiveModal] = useState<string | null>(null);
return (
<div>
<button onClick={() => setActiveModal('profile')}>
Edit Profile
</button>
<button onClick={() => setActiveModal('settings')}>
Settings
</button>
{/* Only load the active modal */}
<Suspense fallback={null}>
{activeModal === 'profile' && (
<UserProfileModal onClose={() => setActiveModal(null)} />
)}
{activeModal === 'settings' && (
<SettingsModal onClose={() => setActiveModal(null)} />
)}
</Suspense>
</div>
);
}🔄 Tab Pattern
const OverviewTab = lazy(() => import('./tabs/OverviewTab'));
const AnalyticsTab = lazy(() => import('./tabs/AnalyticsTab'));
const SettingsTab = lazy(() => import('./tabs/SettingsTab'));
const UsersTab = lazy(() => import('./tabs/UsersTab'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const tabs = {
overview: OverviewTab,
analytics: AnalyticsTab,
settings: SettingsTab,
users: UsersTab
};
const ActiveTabComponent = tabs[activeTab];
return (
<div>
<div className="tabs">
<button onClick={() => setActiveTab('overview')}>Overview</button>
<button onClick={() => setActiveTab('analytics')}>Analytics</button>
<button onClick={() => setActiveTab('settings')}>Settings</button>
<button onClick={() => setActiveTab('users')}>Users</button>
</div>
<Suspense fallback={<TabSkeleton />}>
<ActiveTabComponent />
</Suspense>
</div>
);
}📱 Below-the-Fold Pattern
import { useInView } from 'react-intersection-observer';
const HeavyFooter = lazy(() => import('./HeavyFooter'));
const CommentsSection = lazy(() => import('./CommentsSection'));
function BlogPost() {
const { ref, inView } = useInView({
triggerOnce: true,
rootMargin: '200px' // Load 200px before visible
});
return (
<article>
<h1>Blog Post Title</h1>
<div className="content">Article content...</div>
{/* Load comments when user scrolls near */}
<div ref={ref}>
{inView && (
<Suspense fallback={<CommentsSkeleton />}>
<CommentsSection />
</Suspense>
)}
</div>
{/* Footer loads when visible */}
<Suspense fallback={<div>Loading...</div>}>
<HeavyFooter />
</Suspense>
</article>
);
}🎯 Conditional Feature Pattern
// Feature flags with code splitting
const AdminPanel = lazy(() => import('./AdminPanel'));
const BetaFeature = lazy(() => import('./BetaFeature'));
function Dashboard() {
const { user } = useAuth();
const { features } = useFeatureFlags();
return (
<div>
<h1>Dashboard</h1>
{/* Only load admin panel for admins */}
{user.role === 'admin' && (
<Suspense fallback={<AdminPanelSkeleton />}>
<AdminPanel />
</Suspense>
)}
{/* Only load beta features when enabled */}
{features.betaEnabled && (
<Suspense fallback={<div>Loading beta feature...</div>}>
<BetaFeature />
</Suspense>
)}
</div>
);
}🚀 Preloading on Hover
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
const [show, setShow] = useState(false);
// Preload on hover
const handleMouseEnter = () => {
import('./HeavyComponent');
};
return (
<div>
<button
onMouseEnter={handleMouseEnter}
onClick={() => setShow(true)}
>
Show Component
</button>
{show && (
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}📊 Real-World Examples
Stripe Dashboard
// Stripe splits:
// - Chart libraries (only on analytics page)
// - Payment form components (only when creating payment)
// - Advanced filters (only when clicked)
const AdvancedFilters = lazy(() => import('./AdvancedFilters'));
const PaymentForm = lazy(() => import('./PaymentForm'));
const AnalyticsCharts = lazy(() => import('./AnalyticsCharts'));Figma
// Figma splits:
// - Plugin UI (only when plugin opens)
// - Export dialog (only when exporting)
// - Color picker (only when editing colors)
const PluginUI = lazy(() => import('./PluginUI'));
const ExportDialog = lazy(() => import('./ExportDialog'));
const ColorPicker = lazy(() => import('./ColorPicker'));🎨 Custom Lazy Component
interface LazyComponentProps {
load: () => Promise<{ default: React.ComponentType<any> }>;
fallback?: React.ReactNode;
children: React.ReactNode;
}
function LazyComponent({ load, fallback, children }: LazyComponentProps) {
const Component = lazy(load);
return (
<Suspense fallback={fallback || <Spinner />}>
<Component>{children}</Component>
</Suspense>
);
}
// Usage
<LazyComponent
load={() => import('./HeavyComponent')}
fallback={<Skeleton />}
>
<p>Content</p>
</LazyComponent>🎯 Decision Tree
Should I split this component?
Is it > 50 KB?
├─ Yes → Split it ✅
└─ No → Next question
Is it conditionally rendered?
├─ Yes → Split it ✅
└─ No → Next question
Is it below the fold?
├─ Yes → Split it ✅
└─ No → Next question
Is it critical for first render?
├─ Yes → Don't split ❌
└─ No → Consider splitting ⚠️📚 Key Takeaways
- Split heavy components - > 50 KB should be lazy loaded
- Conditional rendering - Perfect for code splitting
- Preload on hover - Better UX than loading on click
- Monitor bundle size - Use webpack-bundle-analyzer
- Provide fallbacks - Skeleton loaders > spinners
- Test on 3G - Slow networks expose issues
- Measure impact - Track load times
Component-based splitting typically saves 30-40% of your bundle size. Start with the biggest components! 🚀