Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 023 / 100

Dynamic import() for JavaScript & TypeScript developers

Load code only when it's needed.

Expona Admin7 min read

Dynamic import()

Most of the time, we bundle our entire JavaScript application into one or two files, load them upfront, and hope the user's browser can parse them quickly. But what if you could load code only when it's actually needed? Dynamic import() lets you fetch and execute modules asynchronously at runtime, rather than bundling everything statically. It returns a promise for the module, enabling you to delay loading heavy dependencies until the moment they're required.

Returns a Promise for the Module

The core concept of dynamic import is straightforward: instead of using a static import statement at the top of your file, you call import() as a function. This returns a Promise that resolves to the module object.

typescript
// Static import (loaded immediately)
import { processPayment } from './payment';

// Dynamic import (loaded when needed)
import('./payment').then((module) => {
  const { processPayment } = module;
  processPayment(100);
});

With modern async/await syntax, this becomes even cleaner:

typescript
async function handleCheckout() {
  const { processPayment } = await import('./payment');
  await processPayment(100);
}

The promise resolves with a module object containing all named exports, plus a default property if the module has a default export. This means you get the same module interface as static imports, but on demand.

I often use this pattern when building user flows where certain features are optional or rarely accessed. For example, in an e-commerce platform, the payment module might not be needed until checkout—why load it on every page load?

Code Splitting in Bundlers

When bundlers like Webpack, Vite, or esbuild encounter a dynamic import, they automatically create a separate chunk (bundle file) for that module. This is called code splitting. Your main bundle stays lean, and each dynamic import creates its own downloadable file that the browser fetches on demand.

typescript
// In your main app file
async function loadEditor() {
  // bundler creates a separate chunk for the editor
  const { Editor } = await import('./editor');
  return new Editor();
}

export { loadEditor };

Your bundler will output something like:

  • main.js (your core application logic)
  • editor.chunk.js (loaded only when loadEditor is called)

This is a game-changer for application performance. A user visiting your homepage doesn't download the entire admin panel, data visualization library, or video editor. Each feature lives in its own bundle and loads only when needed.

I recently optimized a CRM platform using this approach. We had a large charting library that was being bundled with the main application. By wrapping it in a dynamic import and loading it only when users opened the analytics dashboard, we reduced the initial bundle size by 40%. The homepage loaded in half the time.

Different bundlers handle dynamic imports slightly differently, but the principle is the same. Vite, for instance, automatically detects dynamic imports and creates ESM-based chunks that can be loaded efficiently in modern browsers.

Load Heavy Libraries on Demand

Real-world applications often depend on large third-party libraries that aren't needed immediately. Examples include PDF generators, date manipulation libraries, video players, or machine learning models. Dynamic import lets you defer these until they're actually used.

Here's a practical scenario from my experience building a travel booking platform:

typescript
// calendar.ts - Only loaded when user opens the date picker
export async function initializeDatePicker() {
  // moment.js or date-fns is only fetched when needed
  const moment = await import('moment');
  const picker = document.getElementById('date-picker');
  
  picker.addEventListener('focus', () => {
    // render calendar UI using moment
    console.log('Date picker ready', moment.default());
  });
}

And in your main app:

typescript
import { initializeDatePicker } from './calendar';

// The heavy date library isn't loaded yet
document.getElementById('check-in-date').addEventListener('click', async () => {
  // Now we load it, only when the user interacts with the date field
  await initializeDatePicker();
});

Another real-world example: a trading platform I worked on had a complex financial charting library. Most users never opened the advanced chart view, so why load 200KB of charting code for everyone? We wrapped it in a dynamic import that fires only when the user clicks the "Advanced Analytics" button.

You can also provide user feedback while the module loads:

typescript
async function generatePDF(data) {
  const loadingSpinner = document.getElementById('spinner');
  loadingSpinner.style.display = 'block';
  
  try {
    const PDFDocument = await import('pdfkit');
    const doc = new PDFDocument.default();
    
    // Generate PDF with data
    doc.text('Report', 100, 100);
    
    loadingSpinner.style.display = 'none';
  } catch (error) {
    console.error('Failed to load PDF library', error);
    loadingSpinner.style.display = 'none';
  }
}

This approach gives you fine-grained control over when expensive operations happen and helps you build responsive applications where heavy features don't impact the initial load experience.

Works in Browsers and Node

One of the best things about dynamic import is that it works consistently across modern JavaScript environments. Whether you're writing client-side React code, a backend Node.js service, or a hybrid application, the syntax remains the same.

In the Browser:

typescript
// React component that loads a heavy modal only when needed
import { useState } from 'react';

export function Dashboard() {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [SettingsModal, setSettingsModal] = useState(null);

  const handleOpenSettings = async () => {
    if (!SettingsModal) {
      const module = await import('./SettingsModal');
      setSettingsModal(() => module.SettingsModal);
    }
    setIsModalOpen(true);
  };

  return (
    <>
      Open Settings
      {SettingsModal && isModalOpen && }
    
  );
}

In Node.js:

typescript
// server.ts - Load database drivers conditionally
async function connectToDatabase(type: string) {
  let driver;
  
  if (type === 'postgresql') {
    driver = await import('pg');
  } else if (type === 'mongodb') {
    driver = await import('mongodb');
  }
  
  // Use driver...
  return driver;
}

// Only loads the driver you actually need
connectToDatabase(process.env.DB_TYPE);

Node.js uses dynamic import for microservices that might need different adapters or plugins based on configuration. For example, in a ride-hailing backend I built, we had payment providers from different countries. Rather than importing all of them, we dynamically loaded only the provider matching the user's region.

Browser support is excellent in modern environments. Firefox 67+, Chrome 63+, Safari 11.1+, and all modern Node.js versions support dynamic import. If you need to support older browsers, bundlers like Webpack can polyfill the functionality.

Common Mistakes

Not handling errors: Always wrap dynamic imports in try-catch blocks or add .catch() handlers. Network failures or missing modules can leave your app in a broken state.

typescript
// Bad: no error handling
const { MyComponent } = await import('./MyComponent');

// Good: handles errors gracefully
try {
  const { MyComponent } = await import('./MyComponent');
} catch (error) {
  console.error('Failed to load component:', error);
  // Provide fallback UI
}

Forgetting async context: Dynamic import returns a promise, so you must either use await or .then(). Calling it without awaiting won't give you the module synchronously.

typescript
// Bad: module is a Promise, not the actual module
const module = import('./payment');

// Good: unwrap the Promise
const module = await import('./payment');

Over-splitting: Not every import needs to be dynamic. Splitting too aggressively creates many small chunk files, causing more HTTP requests and overhead. Reserve dynamic imports for genuinely heavy or infrequently-used modules.

Ignoring bundler output: Different bundlers handle dynamic imports differently. Always check your final bundle with tools like webpack-bundle-analyzer to confirm chunks are being created as expected.

Key Takeaways

  • Dynamic import() returns a Promise that resolves to a module object, letting you load code at runtime instead of bundling everything upfront.
  • Bundlers automatically create separate chunks for dynamically imported modules, enabling code splitting and smaller initial bundles.
  • Use dynamic import for heavy third-party libraries, optional features, or infrequently accessed code to improve initial load performance.
  • Dynamic import works consistently in browsers and Node.js, making it a reliable tool across full-stack applications.
  • Always handle errors with try-catch or .catch() handlers, and remember that dynamic import is async—you can't use the module synchronously.
  • Balance code splitting: split strategically to avoid creating too many small chunks that fragment your application into unmaintainable pieces.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles