Dynamic Imports — Loading JavaScript Modules on Demand
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
- Dynamic importing everything. Not every module needs to be lazy. Small utility functions (under 1KB) cost more in network overhead than they save.
- Not providing loading states. Dynamic imports take time. Show spinners or skeletons while loading, especially on slow networks.
- Forgetting error boundaries. Dynamic imports can fail (network error, server error). Wrap in error boundaries with fallback UI.
- Over-splitting chunks. Too many tiny chunks (under 5KB) increase HTTP requests and negate the benefit. Bundle related modules together.
- Dynamic importing immediately used modules. If a module is needed on initial render, import it statically. Dynamic imports add latency.
Practice Questions
- How does
import()differ from staticimportstatements? - How does React.lazy work with Suspense for code splitting?
- What is the optimal chunk size for dynamic imports?
- How do you measure the impact of dynamic imports on bundle size?
- 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
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