How to Dynamically Load React Components With Code Splitting and Suspense (With Pros & Cons)
HexShift

HexShift @hexshift

About: Asher Baum - Elixir/Phoenix enthusiast sharing advanced guides on Phoenix Framework, LiveView, WebSocket, Python and Tailwind. Helping devs build reactive, scalable apps with deep, practical insights.

Joined:
Apr 5, 2025

How to Dynamically Load React Components With Code Splitting and Suspense (With Pros & Cons)

Publish Date: Apr 21
0 0

As your React application grows, loading everything up front can slow down performance and hurt user experience. Code splitting allows you to load only what's needed, improving responsiveness. In this article, we’ll explore dynamic component loading using React.lazy and Suspense.

Step 1: Static vs Dynamic Imports

Normally, components are imported statically:

import HeavyComponent from './HeavyComponent';

This means HeavyComponent is bundled into the main JS file, even if the user never visits the route or sees the component.

Step 2: Dynamic Import With React.lazy

To prevent that, use React.lazy with Suspense:

import { Suspense, lazy } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

Step 3: Splitting by Route

This pattern works perfectly with react-router to split by route:

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));

<Routes>
  <Route path="/dashboard" element={
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </Suspense>
  } />
  <Route path="/profile" element={
    <Suspense fallback={<div>Loading...</div>}>
      <Profile />
    </Suspense>
  } />
</Routes>

Step 4: Custom Fallback UI

You can provide a custom loading screen, skeleton loader, or even a shimmer animation:

<Suspense fallback={<MyFancyLoader />}>
  <Dashboard />
</Suspense>

Step 5: Error Handling

To guard against errors in lazy-loaded components, wrap them with an Error Boundary:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <h2>Something went wrong.</h2>;
    }

    return this.props.children;
  }
}
<ErrorBoundary>
  <Suspense fallback={<div>Loading...</div>}>
    <Dashboard />
  </Suspense>
</ErrorBoundary>

Pros & Cons of React Code Splitting

  • ✅ Pros:
    • Reduces initial bundle size for faster load times
    • Improves perceived performance with smart loading indicators
    • Scales better with large apps and many routes
  • ⚠️ Cons:
    • Increased complexity and potential loading glitches if fallback isn't well-designed
    • Extra round-trip network requests for each chunk
    • Requires careful error boundaries to avoid UI failures

Alternatives

  • Loadable Components – A more advanced SSR-friendly library for code splitting
  • React Server Components – Experimental future of server-first rendering

Conclusion

Dynamic imports and code splitting are powerful tools for improving performance in React. With React.lazy and Suspense, you can create seamless, optimized user experiences even as your app scales.

For a much more extensive guide on getting the most out of React portals, check out my full 24-page PDF file on Gumroad. It's available for just $10:

Using React Portals Like a Pro.

If this post helped you, consider supporting me: buymeacoffee.com/hexshift

Comments 0 total

    Add comment