How to Implement React Dynamic Import in Next.js to Improve Initial Load Performance

Use next/dynamic to implement React dynamic import for components, which wraps React.lazy and Suspense with server-side rendering controls and automatic preloading hooks to reduce initial bundle size.

Next.js provides a robust solution for implementing React dynamic import patterns through its next/dynamic API. This utility extends React's native lazy loading capabilities to enable code splitting, server-side rendering awareness, and client-only execution controls. By strategically implementing React dynamic import for heavy components, you can significantly improve initial page load performance while preserving a seamless user experience across server and client environments.

Architecture of React Dynamic Import in Next.js

Core Components and Responsibilities

The implementation of React dynamic import in Next.js relies on several interconnected modules within the packages/next/src/shared/lib/ directory:

  • dynamic() function (in dynamic.tsx): Parses the loader function or options object, creates a Loadable component, and wires SSR flags. It handles the convertModule() logic (lines 34-36) to normalize exports and the noSSR() helper (lines 53-71) for client-only components.

  • Loadable factory (in loadable.shared-runtime.tsx): Implements the runtime loading logic using the react-loadable pattern. It manages promise handling, delay/timeout configurations, and error boundaries. This is where the preload() method is defined (line 56).

  • LoadableContext (in loadable-context.shared-runtime.ts): Propagates module IDs during client-side hydration, enabling the server to execute preloadReady for modules that were already rendered.

  • dynamic-bailout-to-csr.tsx: Provides the fallback component used when SSR is disabled, ensuring the component only renders on the client.

Execution Flow

When you implement React dynamic import using next/dynamic, the following sequence occurs:

  1. User invocation: You call dynamic(() => import('./Heavy')) or pass an options object.
  2. Option parsing: The dynamic() function detects whether the first argument is a Promise, function, or options object, then builds a Loadable configuration.
  3. Module normalization: The loader wraps the resolved value using convertModule() to ensure it always returns an object with a default export.
  4. Subscription creation: Loadable creates a LoadableSubscription that tracks loading state (loading, loaded, error) and notifies React via useSyncExternalStore.
  5. Client rendering: While the promise resolves, the component renders a loading UI (if supplied), then renders the imported component via React.createElement.
  6. SSR handling: If ssr: false is passed, the server returns only the loading placeholder, preventing the component from being rendered during SSR and avoiding unnecessary data transfer.

Implementing React Dynamic Import: Practical Examples

Basic Lazy Component Loading

The simplest way to implement React dynamic import is to pass an import function to next/dynamic:

import dynamic from 'next/dynamic'

const Heavy = dynamic(() => import('../components/Heavy'))

export default function Page() {
  return (
    <>
      <h1>My Page</h1>
      {/* Heavy is rendered only when this part of the UI is reached */}
      <Heavy />
    </>
  )
}

The loader function returns a promise; next/dynamic wraps it with the normalization and subscription logic described in the architecture section.

Custom Loading Indicators

You can provide a custom loading component using the loading option. According to the type definitions in dynamic.tsx (lines 23-29), the loading component receives DynamicOptionsLoadingProps:

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('../components/Chart'), {
  loading: ({ isLoading }) => (isLoading ? <p>Loading chart…</p> : null),
})

This renders your custom UI while the module is being fetched, improving perceived performance.

Disabling Server-Side Rendering

For components that depend on browser-only APIs (like window or document), implement React dynamic import with ssr: false. The noSSR() helper in dynamic.tsx (lines 53-71) disables webpack module hints and ensures the component only renders on the client:

import dynamic from 'next/dynamic'

const Map = dynamic(() => import('../components/Map'), {
  ssr: false,
})

Because ssr is false, the server sends only the loading placeholder, and the map bundle is fetched and executed after hydration.

Programmatic Preloading

To optimize user interactions, preload components before they are needed. The LoadableComponent exposes a preload() method defined in loadable.shared-runtime.tsx (line 56):

import dynamic from 'next/dynamic'

const Modal = dynamic(() => import('../components/Modal'))

// Somewhere in a click handler
function openModal() {
  Modal.preload() // Triggers the import early
  // later …
}

This triggers the dynamic import early, reducing latency when the component actually renders.

Key Source Files in next/dynamic

The React dynamic import implementation in Next.js spans several critical files:

File Role Link
packages/next/src/shared/lib/dynamic.tsx Public dynamic API, option parsing, SSR handling dynamic.tsx
packages/next/src/shared/lib/loadable.shared-runtime.tsx Runtime implementation of Loadable (subscription, preloading) loadable.shared-runtime.tsx
packages/next/src/shared/lib/loadable-context.shared-runtime.ts Context for capturing module IDs during hydration loadable-context.shared-runtime.ts
packages/next/src/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.tsx Fallback component used when SSR is disabled dynamic-bailout-to-csr.tsx
packages/next/src/api/dynamic.ts Exported entry point for the next/dynamic package dynamic.ts

These files together constitute the full stack that enables React dynamic import in a Next.js application, allowing developers to improve initial load performance while preserving server-side rendering capabilities.

Summary

Implementing React dynamic import in Next.js through the next/dynamic API provides a robust mechanism for optimizing initial load performance:

  • Code-splitting by default: Components loaded via dynamic() are automatically split into separate chunks, reducing the initial JavaScript bundle size.
  • SSR-aware loading: The architecture supports full server-side rendering while allowing client-only execution through the ssr: false option, implemented via the noSSR() helper in dynamic.tsx.
  • Loading state management: Custom loading components receive DynamicOptionsLoadingProps (defined in dynamic.tsx lines 23-29) to render appropriate UI during module fetching.
  • Programmatic preloading: The preload() method (available on Loadable components via loadable.shared-runtime.tsx line 56) enables eager fetching of modules before they are rendered.
  • Runtime architecture: The system relies on LoadableSubscription (in loadable.shared-runtime.tsx) and LoadableContext (in loadable-context.shared-runtime.ts) to manage loading states and hydration across server and client.

Frequently Asked Questions

How does React dynamic import differ between standard React and Next.js?

Standard React uses React.lazy() combined with Suspense to implement dynamic imports, but this approach lacks server-side rendering support and requires manual handling of loading states. Next.js extends this pattern through next/dynamic, which adds SSR awareness via the ssr option, automatic loading component support through DynamicOptionsLoadingProps, and preloading capabilities via the preload() method. The implementation spans files like dynamic.tsx and loadable.shared-runtime.tsx to provide a unified API that works across server and client environments.

When should I use the ssr: false option in Next.js dynamic imports?

Use ssr: false when your component depends on browser-only APIs such as window, document, or navigator properties that are undefined during server-side rendering. According to the source code in dynamic.tsx (lines 53-71), the noSSR() helper disables webpack module hints and returns a placeholder that only renders on the client after hydration. This prevents hydration mismatches and reduces server payload size by excluding the component from the initial HTML.

How can I preload a dynamically imported component before the user needs it?

Next.js exposes a preload() method on components created with next/dynamic, allowing you to trigger the import promise before the component renders. As implemented in loadable.shared-runtime.tsx (line 56), calling Component.preload() executes the loader function immediately, caching the result. This is useful for prefetching heavy components on user interactions like hover or click before the actual navigation or display occurs.

What loading options are available when implementing React dynamic import in Next.js?

The next/dynamic API accepts a loading option that receives DynamicOptionsLoadingProps, defined in dynamic.tsx (lines 23-29). This allows you to render custom UI based on the isLoading state, handle loading errors, or delay the loading indicator to prevent flickering on fast networks. You can also omit the loading option to render nothing while the module fetches, or provide a static component that displays immediately during the import process.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →