Front-end Engineering Lab

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

  1. Split heavy components - > 50 KB should be lazy loaded
  2. Conditional rendering - Perfect for code splitting
  3. Preload on hover - Better UX than loading on click
  4. Monitor bundle size - Use webpack-bundle-analyzer
  5. Provide fallbacks - Skeleton loaders > spinners
  6. Test on 3G - Slow networks expose issues
  7. Measure impact - Track load times

Component-based splitting typically saves 30-40% of your bundle size. Start with the biggest components! 🚀

On this page