Next.js’s dynamic imports are not just about "loading components later"; they fundamentally change how your application’s JavaScript bundle is structured and delivered, allowing for significantly faster initial page loads by deferring non-critical code execution.

Let’s see this in action with a simple example. Imagine you have a modal component that’s only displayed when a user clicks a button. Instead of including its JavaScript in the initial bundle, we can dynamically import it.

// pages/index.js
import dynamic from 'next/dynamic';
import { useState } from 'react';

// Dynamically import the Modal component
const DynamicModal = dynamic(() => import('../components/Modal'), {
  loading: () => <p>Loading modal...</p>, // Optional: show a fallback UI
});

function HomePage() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <h1>Welcome to My App</h1>
      <button onClick={() => setShowModal(true)}>Open Modal</button>

      {showModal && <DynamicModal onClose={() => setShowModal(false)} />}
    </div>
  );
}

export default HomePage;

And here’s the components/Modal.js:

// components/Modal.js
export default function Modal({ onClose }) {
  return (
    <div className="modal">
      <h2>This is a Modal</h2>
      <p>Content loaded dynamically!</p>
      <button onClick={onClose}>Close</button>
    </div>
  );
}

When you run npm run build and inspect the generated JavaScript files in the .next/static/chunks directory, you’ll notice a new chunk file created specifically for Modal.js (e.g., _components_Modal_js-xxxxxx.js). This file is not loaded by default when the HomePage first renders. It’s only fetched and executed when showModal becomes true and DynamicModal is rendered for the first time.

This technique is called "code splitting," and Next.js handles it automatically with next/dynamic. The primary problem it solves is the ballooning size of your JavaScript bundle, which directly impacts your Time to Interactive (TTI) and First Contentful Paint (FCP). By default, all your components, even those used infrequently, would be bundled together. Dynamic imports allow you to surgically slice out these components into separate, on-demand JavaScript chunks.

Internally, next/dynamic leverages Webpack’s dynamic import() syntax. When Next.js encounters dynamic(() => import('...')), it configures Webpack to create a separate output file for that module. The loading option you provide is rendered immediately while the actual component’s JavaScript is being fetched over the network. Once the chunk is downloaded and executed, Next.js swaps out the loading component for the actual dynamically imported one. The ssr: false option is useful if a component relies heavily on browser-specific APIs (like window or document) and shouldn’t be rendered on the server.

The true power of next/dynamic lies in its ability to be used with conditional rendering logic, as shown in the HomePage example. This means you can defer loading anything that isn’t immediately necessary for the user’s initial interaction. Think of complex UI elements, third-party widgets, analytics scripts that aren’t critical for initial rendering, or even entire sections of your application that are only accessed after a specific user action.

What most developers don’t realize is that you can pass an object to dynamic with more than just loading. You can also specify ssr: false. This tells Next.js to not attempt to render the component on the server. This is crucial for components that rely on browser-only APIs like window or document. If you try to SSR a component that uses these, you’ll get errors. For example, if your Modal component needed to access window.innerWidth on mount, you’d wrap its import like this: dynamic(() => import('../components/Modal'), { ssr: false, loading: () => <p>Loading...</p> }). This ensures the component is only rendered client-side, avoiding server-side rendering errors and simplifying your server logic.

With dynamic imports, you’re not just making your app faster; you’re fundamentally changing its loading strategy to be more efficient and user-centric.

The next hurdle you’ll encounter is managing the loading states and potential errors during these dynamic fetches.

Want structured learning?

Take the full Nextjs course →