Skip to content

Dynamic Imports — Loading JavaScript Modules on Demand

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Dynamic Imports. We cover key concepts, practical examples, and best practices to help you master this topic.

Dynamic imports load JavaScript modules only when needed, splitting bundles into smaller chunks that load on demand for faster initial pages.

What You'll Learn

By the end of this tutorial, you'll understand how dynamic imports work in JavaScript, how to use them with different frameworks, how to configure Code Splitting, and how to measure the impact on bundle size.

Why It Matters

JavaScript is the most expensive resource on the web. A single page can ship 500KB+ of unused JS. Dynamic imports defer non-critical code, reducing parse, compile, and execution time on initial page load.

Real-World Use

A dashboard application has 20 different chart types. Instead of loading all charting libraries upfront, each chart component dynamically imports its renderer when added to the page. Initial bundle drops from 800KB to 200KB.

Dynamic Import Flow

graph TD
    A[Initial Page Load] --> B[Load core
JavaScript bundle] B --> C[Execute only
critical code] C --> D[User action
triggers feature] D --> E[Dynamic import()
loads module] E --> F[Network request
for chunk file] F --> G[Parse + compile
module] G --> H[Execute module
when ready] H --> I[Feature available] style B fill:#27ae60,color:#fff style E fill:#4a90d9,color:#fff style F fill:#e67e22,color:#fff style I fill:#27ae60,color:#fff

Basic Dynamic Imports

// Static import (loaded eagerly with main bundle)
import { heavyFunction } from './heavy-module';

// Dynamic import (loaded on demand)
async function loadHeavyFeature() {
    const module = await import('./heavy-module.js');
    module.heavyFunction();
}

// Dynamic import with destructuring
async function showChart() {
    const { renderChart } = await import('./charts/line-chart.js');
    renderChart('container', data);
}

// Dynamic import with default export
async function loadEditor() {
    const RichTextEditor = (await import('./editor.js')).default;
    const editor = new RichTextEditor('#editor');
}

// Dynamic import with error handling
async function loadOptionalFeature() {
    try {
        const utils = await import('./utils/optional.js');
        return utils.processData(data);
    } catch (err) {
        console.warn('Optional feature not available:', err);
        return fallbackProcess(data);
    }
}

Framework Dynamic Imports

// React: React.lazy + Suspense
import React, { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./charts/HeavyChart'));
const PDFPreview = lazy(() => import('./PDFPreview'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));

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

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

            <Suspense fallback={<div className="skeleton" />}>
                {showChart && <HeavyChart data={data} />}
            </Suspense>

            {/* Lazy loading below the fold */}
            <Suspense fallback={<div>Loading PDF viewer...</div>}>
                <PDFPreview url="/doc/report.pdf" />
            </Suspense>

            {/* Route-based code splitting */}
            <Suspense fallback={<Loading />}>
                <VideoPlayer src="tutorial.mp4" />
            </Suspense>
        </div>
    );
}
// Vue 3: defineAsyncComponent
import { defineAsyncComponent } from 'vue';

const AsyncChart = defineAsyncComponent(() =>
    import('./components/Chart.vue')
);

const AsyncDataTable = defineAsyncComponent({
    loader: () => import('./components/DataTable.vue'),
    loadingComponent: LoadingSpinner,
    errorComponent: ErrorDisplay,
    delay: 200,
    timeout: 3000
});

// Usage in template
// <AsyncChart :data="chartData" />
// <AsyncDataTable :items="items" />

Webpack Chunk Configuration

// webpack.config.js — Chunk naming and splitting
module.exports = {
    output: {
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].chunk.js',
        path: path.resolve(__dirname, 'dist'),
        publicPath: '/_next/static/chunks/'
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 25,
            minSize: 20000,
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'all',
                    priority: 10
                },
                common: {
                    minChunks: 2,
                    priority: 5,
                    reuseExistingChunk: true
                },
                // Separate heavy libraries
                chartLibrary: {
                    test: /[\\/]node_modules[\\/](chart\.js|d3)[\\/]/,
                    name: 'chart-vendor',
                    chunks: 'async',
                    priority: 15
                }
            }
        }
    }
};

Measuring Bundle Impact

// Bundle size monitoring
class BundleMonitor {
    constructor() {
        this.initialSize = 0;
        this.dynamicLoads = [];
        this.performanceEntries = [];
    }

    measureInitial() {
        // Measure initial JS bundle size
        const scripts = document.querySelectorAll('script[src]');
        let totalSize = 0;

        scripts.forEach(script => {
            // Estimate from file name or fetch header
            totalSize += script.dataset.size || 0;
        });

        this.initialSize = totalSize;
        console.log(`Initial JS bundle: ~${(totalSize / 1024).toFixed(1)}KB`);
    }

    trackDynamicImport(moduleName, loadTime, chunkSize) {
        this.dynamicLoads.push({
            module: moduleName,
            loadTime,
            chunkSize,
            timestamp: Date.now()
        });

        console.log(`Dynamic import: ${moduleName} (${(chunkSize / 1024).toFixed(1)}KB, ${loadTime}ms)`);

        // Update cumulative stats
        this.renderStats();
    }

    renderStats() {
        const totalDynamicSize = this.dynamicLoads.reduce(
            (sum, load) => sum + load.chunkSize, 0
        );

        console.log(`
        Bundle Stats:
        - Initial: ${(this.initialSize / 1024).toFixed(1)}KB
        - Dynamic loaded: ${(totalDynamicSize / 1024).toFixed(1)}KB
        - Dynamic deferred: ${this.dynamicLoads.length} modules
        - Total avoided: ${((this.initialSize + totalDynamicSize) / 1024).toFixed(1)}KB
        `);
    }
}

const monitor = new BundleMonitor();

// Usage
async function loadCharts() {
    const start = performance.now();
    const { ChartComponent } = await import('./charts/ChartComponent.js');

    const loadTime = performance.now() - start;
    const chunkSize = 45000; // Would get from network monitoring

    monitor.trackDynamicImport('ChartComponent', loadTime, chunkSize);

    return ChartComponent;
}

Common Mistakes

  1. Dynamic importing everything. Not every module needs to be lazy. Small utility functions (under 1KB) cost more in network overhead than they save.
  2. Not providing loading states. Dynamic imports take time. Show spinners or skeletons while loading, especially on slow networks.
  3. Forgetting error boundaries. Dynamic imports can fail (network error, server error). Wrap in error boundaries with fallback UI.
  4. Over-splitting chunks. Too many tiny chunks (under 5KB) increase HTTP requests and negate the benefit. Bundle related modules together.
  5. Dynamic importing immediately used modules. If a module is needed on initial render, import it statically. Dynamic imports add latency.

Practice Questions

  1. How does import() differ from static import statements?
  2. How does React.lazy work with Suspense for code splitting?
  3. What is the optimal chunk size for dynamic imports?
  4. How do you measure the impact of dynamic imports on bundle size?
  5. When should you NOT use dynamic imports?

Challenge: Refactor a large JavaScript application to use dynamic imports: identify 5 modules that can be lazy loaded, implement React.lazy or dynamic import() for each, configure Webpack chunk splitting, and benchmark the before/after bundle size and load time.

FAQ

Does dynamic import work with all bundlers?

Yes. Webpack, Vite, Rollup, and Parcel all support dynamic import() syntax. Some may require specific configuration for chunk naming.

What is the network overhead of dynamic imports?

Small. Each dynamic import adds an HTTP request (2-3KB overhead). Group related modules into larger chunks (20-50KB) to minimize requests.

Can dynamic imports be preloaded?

Yes. Use or webpack magic comments (/* webpackPreload: true */) to preload critical dynamic chunks.

How does dynamic import affect caching?

Each chunk gets a content hash. Changed modules produce a new hash, so unchanged chunks stay cached. This is ideal for long-term caching.

Can I use dynamic imports with Node.js?

Yes. Node.js supports dynamic import() natively. It's useful for loading plugins, config files, or optional dependencies on demand.

Mini Project

Build a dashboard with dynamic imports: implement React.lazy for 3 chart components, dynamic import() for a PDF viewer and export function, bundle analysis with Webpack Bundle Analyzer, and a performance dashboard showing chunk sizes and load times.

What's Next

You've mastered dynamic imports. Now learn about React.lazy & Suspense for component-level code splitting in React applications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro