DEV Community

Tarun Moorjani
Tarun Moorjani

Posted on

Code Splitting TypeScript React: Lazy Loading Done Right

My React app was 2.5MB. Users on slow connections waited 15 seconds to see anything. Time to Interactive was abysmal. Google Lighthouse scores were red across the board.

I knew I needed code splitting. I'd heard about React.lazy() and dynamic imports. But every tutorial showed JavaScript examples, and when I tried to add TypeScript, nothing worked:

// ❌ This breaks everything
const Dashboard = React.lazy(() => import('./Dashboard'));
// Type error: Cannot find module './Dashboard' or its corresponding type declarations
Enter fullscreen mode Exit fullscreen mode

The types didn't import correctly. Suspense boundaries felt clunky. Preloading was a mystery. And I had no idea if my splitting strategy actually worked.

After months of optimization, I got my initial bundle down to 180KB. Time to Interactive dropped from 15 seconds to 2 seconds. Here's everything I learned about code splitting with TypeScript.

The Fundamentals: How Code Splitting Works

Before diving into TypeScript, understand what's happening:

Without code splitting:

app.js (2.5MB) → User waits for entire app before seeing anything
Enter fullscreen mode Exit fullscreen mode

With code splitting:

main.js (180KB)    → User sees app immediately
dashboard.js (400KB) → Loads when user navigates to dashboard
admin.js (600KB)    → Loads only for admin users
charts.js (350KB)   → Loads when user opens charts
Enter fullscreen mode Exit fullscreen mode

Code splitting transforms one massive bundle into smaller chunks that load on-demand.

Pattern 1: Basic Type-Safe Lazy Loading

The Problem with Simple Lazy Loading

// ❌ Type inference breaks
const Dashboard = React.lazy(() => import('./Dashboard'));

// Later...
<Dashboard />  // No prop autocomplete, no type checking
Enter fullscreen mode Exit fullscreen mode

The Solution: Proper Component Types

// Dashboard.tsx
interface DashboardProps {
  userId: string;
  onLogout: () => void;
}

export default function Dashboard({ userId, onLogout }: DashboardProps) {
  return <div>Dashboard for {userId}</div>;
}

// App.tsx
import { lazy, ComponentType } from 'react';

// ✅ Explicitly type the lazy component
const Dashboard = lazy<ComponentType<DashboardProps>>(
  () => import('./Dashboard')
);

// Usage - full type safety!
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Dashboard 
        userId="123" 
        onLogout={handleLogout}
        // TypeScript knows the props!
      />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Generic Lazy Loader Helper

// utils/lazy.ts
import { lazy, ComponentType } from 'react';

/**
 * Type-safe wrapper around React.lazy
 */
export function lazyLoad<T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>
): T {
  return lazy(factory) as unknown as T;
}

// Usage
const Dashboard = lazyLoad<ComponentType<DashboardProps>>(
  () => import('./Dashboard')
);

// Even cleaner - infer from import
const Settings = lazyLoad(() => import('./Settings'));
// TypeScript infers the component type!
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Route-Based Code Splitting

Split by routes for maximum impact.

Basic Route Splitting

// routes.tsx
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';

// Lazy load route components
const HomePage = lazy(() => import('./pages/Home'));
const DashboardPage = lazy(() => import('./pages/Dashboard'));
const ProfilePage = lazy(() => import('./pages/Profile'));
const SettingsPage = lazy(() => import('./pages/Settings'));

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <HomePage />,
  },
  {
    path: '/dashboard',
    element: <DashboardPage />,
  },
  {
    path: '/profile',
    element: <ProfilePage />,
  },
  {
    path: '/settings',
    element: <SettingsPage />,
  },
];
Enter fullscreen mode Exit fullscreen mode

Suspense Wrapper for Routes

// components/RouteWithSuspense.tsx
import { Suspense, ComponentType } from 'react';

interface RouteWithSuspenseProps {
  component: ComponentType;
  fallback?: React.ReactNode;
}

export function RouteWithSuspense({ 
  component: Component, 
  fallback = <PageLoader /> 
}: RouteWithSuspenseProps) {
  return (
    <Suspense fallback={fallback}>
      <Component />
    </Suspense>
  );
}

// Usage
const routes: RouteObject[] = [
  {
    path: '/dashboard',
    element: <RouteWithSuspense component={DashboardPage} />,
  },
];
Enter fullscreen mode Exit fullscreen mode

Nested Route Splitting

// App.tsx
import { Outlet } from 'react-router-dom';

const AdminLayout = lazy(() => import('./layouts/AdminLayout'));
const UserManagement = lazy(() => import('./pages/admin/UserManagement'));
const Analytics = lazy(() => import('./pages/admin/Analytics'));
const Reports = lazy(() => import('./pages/admin/Reports'));

export const routes: RouteObject[] = [
  {
    path: '/admin',
    element: (
      <Suspense fallback={<LayoutSkeleton />}>
        <AdminLayout />
      </Suspense>
    ),
    children: [
      {
        path: 'users',
        element: (
          <Suspense fallback={<PageSkeleton />}>
            <UserManagement />
          </Suspense>
        ),
      },
      {
        path: 'analytics',
        element: (
          <Suspense fallback={<PageSkeleton />}>
            <Analytics />
          </Suspense>
        ),
      },
      {
        path: 'reports',
        element: (
          <Suspense fallback={<PageSkeleton />}>
            <Reports />
          </Suspense>
        ),
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Component-Based Code Splitting

Split heavy components, not routes.

Identifying Split Candidates

// Heavy components to split:
// - Rich text editors
// - Chart libraries
// - Data tables
// - Modals (especially heavy ones)
// - Admin panels
// - Feature-flagged components

// Dashboard.tsx
import { lazy, useState } from 'react';

// ✅ Split heavy chart component
const ChartComponent = lazy(() => import('./ChartComponent'));

// ✅ Split heavy data table
const DataTable = lazy(() => import('./DataTable'));

export function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <h1>Dashboard</h1>

      <button onClick={() => setShowChart(true)}>
        Show Chart
      </button>

      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <ChartComponent data={chartData} />
        </Suspense>
      )}

      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Modal Code Splitting

// Modals are perfect for code splitting - they're not needed until opened

interface UserModalProps {
  userId: string;
  onClose: () => void;
}

const UserModal = lazy(() => import('./modals/UserModal'));

function UserList() {
  const [selectedUserId, setSelectedUserId] = useState<string | null>(null);

  return (
    <div>
      {users.map(user => (
        <button key={user.id} onClick={() => setSelectedUserId(user.id)}>
          {user.name}
        </button>
      ))}

      {selectedUserId && (
        <Suspense fallback={<ModalSkeleton />}>
          <UserModal 
            userId={selectedUserId} 
            onClose={() => setSelectedUserId(null)} 
          />
        </Suspense>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Library Code Splitting

Split large dependencies dynamically.

Dynamic Library Imports

// utils/dynamicImports.ts

/**
 * Lazy load Chart.js only when needed
 */
export async function loadChartLibrary() {
  const { Chart, registerables } = await import('chart.js');
  Chart.register(...registerables);
  return Chart;
}

/**
 * Lazy load date-fns only when needed
 */
export async function formatDate(date: Date, format: string) {
  const { format: formatFn } = await import('date-fns');
  return formatFn(date, format);
}

/**
 * Lazy load lodash functions
 */
export async function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): Promise<T & { cancel: () => void }> {
  const { default: debounce } = await import('lodash/debounce');
  return debounce(func, wait);
}

// Usage
function ChartComponent() {
  const [Chart, setChart] = useState<typeof import('chart.js').Chart | null>(null);

  useEffect(() => {
    loadChartLibrary().then(setChart);
  }, []);

  if (!Chart) return <div>Loading chart...</div>;

  return <canvas ref={canvasRef} />;
}
Enter fullscreen mode Exit fullscreen mode

Code Splitting Heavy Utilities

// utils/markdown.ts
/**
 * Don't import markdown library at app startup
 * Load it only when rendering markdown
 */
export async function renderMarkdown(markdown: string): Promise<string> {
  const { marked } = await import('marked');
  return marked(markdown);
}

// Usage
function MarkdownPreview({ content }: { content: string }) {
  const [html, setHtml] = useState<string>('');

  useEffect(() => {
    renderMarkdown(content).then(setHtml);
  }, [content]);

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Preloading Strategies

Load code before it's needed.

Preload on Hover

// components/Link.tsx
import { Link as RouterLink, LinkProps } from 'react-router-dom';
import { ComponentType } from 'react';

interface PreloadableLinkProps extends LinkProps {
  preload?: () => Promise<{ default: ComponentType }>;
}

export function Link({ preload, ...props }: PreloadableLinkProps) {
  const handleMouseEnter = () => {
    if (preload) {
      // Start loading when user hovers
      preload();
    }
  };

  return (
    <RouterLink 
      {...props} 
      onMouseEnter={handleMouseEnter}
      onFocus={handleMouseEnter}
    />
  );
}

// Usage
<Link 
  to="/dashboard" 
  preload={() => import('./pages/Dashboard')}
>
  Go to Dashboard
</Link>
Enter fullscreen mode Exit fullscreen mode

Preload on Idle

// utils/preload.ts
/**
 * Preload component during browser idle time
 */
export function preloadOnIdle(
  factory: () => Promise<{ default: ComponentType<any> }>
): void {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      factory();
    });
  } else {
    // Fallback for browsers without requestIdleCallback
    setTimeout(() => {
      factory();
    }, 1);
  }
}

// App.tsx
useEffect(() => {
  // Preload likely next pages during idle time
  preloadOnIdle(() => import('./pages/Dashboard'));
  preloadOnIdle(() => import('./pages/Profile'));
}, []);
Enter fullscreen mode Exit fullscreen mode

Preload on Route Match

// hooks/usePreloadRoutes.ts
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';

interface RoutePreloadMap {
  [path: string]: () => Promise<{ default: ComponentType<any> }>;
}

const preloadMap: RoutePreloadMap = {
  '/': () => import('./pages/Dashboard'),
  '/profile': () => import('./pages/Settings'),
  '/dashboard': () => import('./pages/Analytics'),
};

export function usePreloadRoutes() {
  const location = useLocation();

  useEffect(() => {
    // Preload likely next route based on current route
    const nextRoute = preloadMap[location.pathname];
    if (nextRoute) {
      preloadOnIdle(nextRoute);
    }
  }, [location.pathname]);
}
Enter fullscreen mode Exit fullscreen mode

Aggressive Preloading

// Preload everything after initial render
function App() {
  useEffect(() => {
    // Wait for initial render to complete
    setTimeout(() => {
      // Preload all routes
      import('./pages/Dashboard');
      import('./pages/Profile');
      import('./pages/Settings');
      import('./pages/Admin');
    }, 2000);
  }, []);

  return <Router />;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 6: Type-Safe Suspense Boundaries

Nested Suspense Boundaries

// Different loading states for different parts
function App() {
  return (
    <Suspense fallback={<AppShell />}>
      <Layout>
        <Suspense fallback={<Sidebar.Skeleton />}>
          <Sidebar />
        </Suspense>

        <Main>
          <Suspense fallback={<PageLoader />}>
            <Routes />
          </Suspense>
        </Main>
      </Layout>
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Error Boundaries with Suspense

// ErrorBoundary.tsx
import { Component, ReactNode } from 'react';

interface Props {
  fallback: (error: Error) => ReactNode;
  children: ReactNode;
}

interface State {
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error): State {
    return { error };
  }

  render() {
    if (this.state.error) {
      return this.props.fallback(this.state.error);
    }

    return this.props.children;
  }
}

// Usage - wrap Suspense with ErrorBoundary
function App() {
  return (
    <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
      <Suspense fallback={<Loading />}>
        <Dashboard />
      </Suspense>
    </ErrorBoundary>
  );
}
Enter fullscreen mode Exit fullscreen mode

Custom Suspense Component

// components/SuspenseWithError.tsx
import { Suspense, ReactNode } from 'react';
import { ErrorBoundary } from './ErrorBoundary';

interface SuspenseWithErrorProps {
  children: ReactNode;
  fallback: ReactNode;
  errorFallback?: (error: Error) => ReactNode;
}

export function SuspenseWithError({
  children,
  fallback,
  errorFallback = (error) => <div>Error: {error.message}</div>,
}: SuspenseWithErrorProps) {
  return (
    <ErrorBoundary fallback={errorFallback}>
      <Suspense fallback={fallback}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

// Usage
<SuspenseWithError
  fallback={<PageLoader />}
  errorFallback={(error) => <ErrorPage error={error} />}
>
  <Dashboard />
</SuspenseWithError>
Enter fullscreen mode Exit fullscreen mode

Pattern 7: Handling Shared Types

Type-Only Imports

// types/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

// Dashboard.tsx
// ✅ Type-only import - doesn't affect bundle
import type { User } from '../types/user';

export default function Dashboard({ user }: { user: User }) {
  return <div>Hello {user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Shared Type Definitions

// Create a types-only package for shared types
// This doesn't get bundled with code

// types/index.ts
export type { User } from './user';
export type { Post } from './post';
export type { Comment } from './comment';

// Components import types without bundling them
import type { User, Post } from '@/types';
Enter fullscreen mode Exit fullscreen mode

Avoiding Type Duplication

// ❌ Bad - duplicates types
// Dashboard.tsx
interface User {
  id: string;
  name: string;
}

// Profile.tsx
interface User {
  id: string;
  name: string;
}

// ✅ Good - shared types
// types/user.ts
export interface User {
  id: string;
  name: string;
}

// Both import the same type
import type { User } from '@/types';
Enter fullscreen mode Exit fullscreen mode

Pattern 8: Dynamic Import Utilities

Type-Safe Dynamic Component Loader

// utils/loadComponent.ts
import { ComponentType } from 'react';

interface LoadComponentOptions {
  fallback?: React.ReactNode;
  onError?: (error: Error) => void;
}

export function loadComponent<P = {}>(
  factory: () => Promise<{ default: ComponentType<P> }>,
  options: LoadComponentOptions = {}
): ComponentType<P> {
  const LazyComponent = React.lazy(factory);

  return function LoadedComponent(props: P) {
    return (
      <ErrorBoundary
        fallback={(error) => {
          options.onError?.(error);
          return <div>Failed to load component</div>;
        }}
      >
        <Suspense fallback={options.fallback ?? <div>Loading...</div>}>
          <LazyComponent {...props} />
        </Suspense>
      </ErrorBoundary>
    );
  };
}

// Usage
const Dashboard = loadComponent(
  () => import('./Dashboard'),
  {
    fallback: <DashboardSkeleton />,
    onError: (error) => console.error('Failed to load dashboard', error),
  }
);
Enter fullscreen mode Exit fullscreen mode

Retry Logic for Failed Chunks

// utils/retryImport.ts
interface ImportRetryOptions {
  maxRetries?: number;
  delay?: number;
}

/**
 * Retry dynamic imports that fail (common with network issues)
 */
export async function retryImport<T>(
  factory: () => Promise<T>,
  { maxRetries = 3, delay = 1000 }: ImportRetryOptions = {}
): Promise<T> {
  let lastError: Error;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await factory();
    } catch (error) {
      lastError = error as Error;

      // Wait before retrying
      if (i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
      }
    }
  }

  throw lastError!;
}

// Usage
const Dashboard = lazy(() =>
  retryImport(() => import('./Dashboard'))
);
Enter fullscreen mode Exit fullscreen mode

Prefetch Helper

// utils/prefetch.ts
/**
 * Prefetch a component without rendering it
 */
export function prefetchComponent(
  factory: () => Promise<{ default: ComponentType<any> }>
): void {
  factory().catch(() => {
    // Silently fail - prefetch is optimization, not critical
  });
}

// Prefetch on user interaction
function Navigation() {
  return (
    <nav>
      <a
        href="/dashboard"
        onMouseEnter={() => prefetchComponent(() => import('./pages/Dashboard'))}
      >
        Dashboard
      </a>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pattern 9: Bundle Analysis and Optimization

Analyzing Bundle Size

// Install bundle analyzer
// npm install --save-dev webpack-bundle-analyzer

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Vendor splitting
          'react-vendor': ['react', 'react-dom'],
          'router': ['react-router-dom'],
          'query': ['@tanstack/react-query'],
        },
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Manual Chunk Configuration

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // Split node_modules into separate chunks
          if (id.includes('node_modules')) {
            if (id.includes('react') || id.includes('react-dom')) {
              return 'react-vendor';
            }
            if (id.includes('lodash')) {
              return 'lodash-vendor';
            }
            if (id.includes('chart')) {
              return 'chart-vendor';
            }
            return 'vendor';
          }

          // Split pages into separate chunks
          if (id.includes('/pages/')) {
            const pageName = id.split('/pages/')[1].split('/')[0];
            return `page-${pageName}`;
          }
        },
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Pattern 10: Testing Lazy Components

Testing with Suspense

// Dashboard.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { Suspense } from 'react';

// Import actual component for testing, not lazy version
import Dashboard from './Dashboard';

test('renders dashboard', async () => {
  render(
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </Suspense>
  );

  // Wait for suspense to resolve
  await waitFor(() => {
    expect(screen.getByText('Dashboard')).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Mock Lazy Imports

// __mocks__/lazyComponent.ts
import { ComponentType } from 'react';

export function mockLazy<T extends ComponentType<any>>(
  component: T
): T {
  return component;
}

// Dashboard.test.tsx
jest.mock('./Dashboard', () => ({
  default: () => <div>Dashboard Mock</div>,
}));

test('renders mocked lazy component', () => {
  const Dashboard = lazy(() => import('./Dashboard'));

  render(
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </Suspense>
  );

  expect(screen.getByText('Dashboard Mock')).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Complete Implementation

// App.tsx - Complete code splitting setup
import { lazy, Suspense, useEffect } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { ErrorBoundary } from './components/ErrorBoundary';
import { retryImport } from './utils/retryImport';
import { preloadOnIdle } from './utils/preload';

// Lazy load pages
const HomePage = lazy(() => retryImport(() => import('./pages/Home')));
const DashboardPage = lazy(() => retryImport(() => import('./pages/Dashboard')));
const ProfilePage = lazy(() => retryImport(() => import('./pages/Profile')));
const SettingsPage = lazy(() => retryImport(() => import('./pages/Settings')));
const AdminPage = lazy(() => retryImport(() => import('./pages/Admin')));

// Preload map
const preloadMap: Record<string, () => void> = {
  '/': () => preloadOnIdle(() => import('./pages/Dashboard')),
  '/dashboard': () => preloadOnIdle(() => import('./pages/Profile')),
};

function App() {
  const location = useLocation();

  // Preload next likely page
  useEffect(() => {
    const preload = preloadMap[location.pathname];
    preload?.();
  }, [location.pathname]);

  return (
    <ErrorBoundary
      fallback={(error) => (
        <div>
          <h1>Application Error</h1>
          <p>{error.message}</p>
          <button onClick={() => window.location.reload()}>
            Reload
          </button>
        </div>
      )}
    >
      <Routes>
        <Route
          path="/"
          element={
            <Suspense fallback={<PageLoader />}>
              <HomePage />
            </Suspense>
          }
        />
        <Route
          path="/dashboard"
          element={
            <Suspense fallback={<PageLoader />}>
              <DashboardPage />
            </Suspense>
          }
        />
        <Route
          path="/profile"
          element={
            <Suspense fallback={<PageLoader />}>
              <ProfilePage />
            </Suspense>
          }
        />
        <Route
          path="/settings"
          element={
            <Suspense fallback={<PageLoader />}>
              <SettingsPage />
            </Suspense>
          }
        />
        <Route
          path="/admin"
          element={
            <Suspense fallback={<PageLoader />}>
              <AdminPage />
            </Suspense>
          }
        />
      </Routes>
    </ErrorBoundary>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Performance Metrics

Before Code Splitting

Initial Bundle: 2.5MB
Time to Interactive: 15s (3G)
Lighthouse Score: 23/100
First Contentful Paint: 8s
Enter fullscreen mode Exit fullscreen mode

After Code Splitting

Initial Bundle: 180KB
Time to Interactive: 2s (3G)
Lighthouse Score: 94/100
First Contentful Paint: 1.2s

Route Chunks:
- Dashboard: 400KB
- Admin: 600KB
- Charts: 350KB
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Split at Route Level First

Routes are natural split points. Users navigate between them.

2. Split Heavy Libraries

Chart.js, Markdown parsers, Rich text editors—split them all.

3. Don't Over-Split

Splitting every component creates overhead. Split things > 50KB.

4. Preload Aggressively

Preload on hover, idle time, or based on user patterns.

5. Monitor Bundle Sizes

Use bundle analyzer to find optimization opportunities.

6. Handle Errors Gracefully

Always wrap Suspense with ErrorBoundary.

7. Test Lazy Components

Test the actual components, not the lazy wrappers.

Common Mistakes

❌ Mistake 1: Splitting Too Small

// ❌ Bad - overhead exceeds benefit
const Button = lazy(() => import('./Button')); // 2KB component
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 2: No Error Handling

// ❌ Bad - crashes on failed chunk load
<Suspense fallback={<Loading />}>
  <Dashboard />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 3: Blocking Renders

// ❌ Bad - entire app waits for dashboard
function App() {
  const Dashboard = lazy(() => import('./Dashboard'));
  return <Dashboard />;
}
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 4: Not Using Type-Only Imports

// ❌ Bad - bundles types with code
import { User } from './types';

// ✅ Good - types are stripped
import type { User } from './types';
Enter fullscreen mode Exit fullscreen mode

Conclusion

Code splitting transforms your app from a monolith to a nimble, fast-loading experience.

The strategy:

  1. Split by routes first
  2. Split heavy components second
  3. Split libraries third
  4. Preload aggressively
  5. Monitor and optimize

The tools:

  • React.lazy() for components
  • Dynamic import() for utilities
  • Bundle analyzer for visibility
  • Preloading for perceived performance

The result:

  • Faster initial load
  • Better Time to Interactive
  • Happier users
  • Higher conversion rates

Start with routes. Measure with Lighthouse. Iterate based on data.

Your users on slow connections will thank you.


What's your bundle size? Run npm run build and share your before/after!

Top comments (0)