Skip to main content
Lazy loading (or code splitting on demand) allows you to load parts of your application only when they’re needed, significantly improving initial load time.

What is Lazy Loading?

Lazy loading defers loading of non-critical resources at page load time. Instead, these resources are loaded at the moment they’re needed. Benefits:
  • Reduced initial bundle size
  • Faster initial page load
  • Better resource utilization
  • Improved user experience
Lazy load anything that’s not immediately visible or needed when the page first loads.

Dynamic Imports

The foundation of lazy loading in webpack is the dynamic import() syntax.
1

Convert static imports to dynamic

Before:
import { heavyFunction } from './heavy-module';

button.addEventListener('click', () => {
  heavyFunction();
});
After:
button.addEventListener('click', async () => {
  const { heavyFunction } = await import('./heavy-module');
  heavyFunction();
});
2

Add loading state

button.addEventListener('click', async () => {
  button.disabled = true;
  button.textContent = 'Loading...';
  
  try {
    const module = await import('./heavy-module');
    module.initialize();
  } catch (error) {
    console.error('Failed to load module:', error);
  } finally {
    button.disabled = false;
    button.textContent = 'Click me';
  }
});
3

Name your chunks

const module = await import(
  /* webpackChunkName: "heavy-module" */
  './heavy-module'
);

Route-Based Lazy Loading

Load components only when users navigate to specific routes:

React Router Example

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Vue Router Example

import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: () => import('./views/Home.vue')
    },
    {
      path: '/about',
      component: () => import(
        /* webpackChunkName: "about" */
        './views/About.vue'
      )
    },
    {
      path: '/dashboard',
      component: () => import(
        /* webpackChunkName: "dashboard" */
        './views/Dashboard.vue'
      )
    }
  ]
});
Route-based splitting is the easiest and most effective way to implement lazy loading.

Component-Based Lazy Loading

React

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<div>Loading component...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Vue 3

import { defineAsyncComponent } from 'vue';

export default {
  components: {
    HeavyComponent: defineAsyncComponent(() =>
      import('./HeavyComponent.vue')
    )
  }
};

Prefetching and Preloading

Optimize when lazy-loaded modules are fetched:

Prefetch

Load during idle time (low priority):
// Load when browser is idle
import(
  /* webpackPrefetch: true */
  './future-module'
);
Generates:
<link rel="prefetch" href="future-module.js">
Use prefetch for modules users will likely need soon, like the next page in a flow.

Preload

Load in parallel with parent (high priority):
// Load in parallel with current page
import(
  /* webpackPreload: true */
  './critical-module'
);
Generates:
<link rel="preload" href="critical-module.js" as="script">
Overusing preload can harm performance. Only preload truly critical resources.

Event-Based Lazy Loading

Load code in response to user interactions:

On Click

const button = document.getElementById('open-modal');

button.addEventListener('click', async () => {
  const { Modal } = await import(
    /* webpackChunkName: "modal" */
    './components/Modal'
  );
  
  const modal = new Modal();
  modal.open();
});

On Scroll

const observer = new IntersectionObserver((entries) => {
  entries.forEach(async (entry) => {
    if (entry.isIntersecting) {
      const { initChart } = await import('./chart');
      initChart(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

const chartElement = document.getElementById('chart');
observer.observe(chartElement);

On Hover

const menuItem = document.getElementById('menu-item');

let modulePromise = null;

menuItem.addEventListener('mouseenter', () => {
  // Start loading on hover
  if (!modulePromise) {
    modulePromise = import('./dropdown-menu');
  }
});

menuItem.addEventListener('click', async () => {
  const { DropdownMenu } = await modulePromise;
  const menu = new DropdownMenu();
  menu.show();
});

Library/Vendor Lazy Loading

Load heavy third-party libraries only when needed:
// Load chart library only when showing charts
async function renderChart(data) {
  const Chart = await import(
    /* webpackChunkName: "chart-library" */
    'chart.js'
  );
  
  return new Chart.default('canvas', {
    type: 'bar',
    data: data
  });
}

// Load PDF library only when exporting
async function exportToPDF() {
  const jsPDF = await import(
    /* webpackChunkName: "pdf-library" */
    'jspdf'
  );
  
  const doc = new jsPDF.default();
  // Generate PDF
}
Heavy libraries like chart.js, moment.js, or PDF generators are perfect candidates for lazy loading.

Lazy Compilation

For development, compile dynamic imports only when they’re requested:
module.exports = {
  mode: 'development',
  experiments: {
    lazyCompilation: true
  },
  cache: {
    type: 'filesystem'
  }
};
Lazy compilation can dramatically reduce development server startup time for applications with many routes.

Error Handling

Handle loading failures gracefully:
async function loadModule() {
  try {
    const module = await import('./optional-module');
    return module.default;
  } catch (error) {
    console.error('Failed to load module:', error);
    // Fallback behavior
    return null;
  }
}

Retry Logic

async function importWithRetry(importFn, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await importFn();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

// Usage
const module = await importWithRetry(() => import('./flaky-module'));

Webpack Magic Comments

Control how chunks are loaded:
import(
  /* webpackChunkName: "my-chunk" */
  /* webpackMode: "lazy" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  './module'
);

Available Comments

CommentDescription
webpackChunkNameName for the chunk
webpackMode"lazy" (default), "lazy-once", "eager", "weak"
webpackPrefetchPrefetch during idle time
webpackPreloadPreload in parallel
webpackExportsOnly include specific exports

Mode Options

// lazy (default) - separate chunk for each import
import(/* webpackMode: "lazy" */ `./locales/${lang}.js`);

// lazy-once - single chunk for all possible modules
import(/* webpackMode: "lazy-once" */ `./locales/${lang}.js`);

// eager - no separate chunk, but async execution
import(/* webpackMode: "eager" */ './module');

// weak - assume module is already loaded
import(/* webpackMode: "weak" */ './module');

Best Practices

1

Split by routes

Implement route-based code splitting as your first optimization:
const routes = [
  { path: '/', component: () => import('./Home') },
  { path: '/about', component: () => import('./About') }
];
2

Lazy load heavy components

Identify and lazy load components that:
  • Are not immediately visible
  • Contain heavy dependencies
  • Are only used by some users
3

Use prefetch wisely

Prefetch modules users will likely need:
import(/* webpackPrefetch: true */ './next-page');
4

Monitor bundle sizes

Use webpack-bundle-analyzer to identify lazy loading opportunities:
npm install --save-dev webpack-bundle-analyzer

Common Patterns

Modal/Dialog

let modalModule = null;

async function openModal() {
  if (!modalModule) {
    modalModule = await import('./Modal');
  }
  return new modalModule.default();
}

Tab Content

const tabs = {
  profile: () => import('./ProfileTab'),
  settings: () => import('./SettingsTab'),
  billing: () => import('./BillingTab')
};

async function switchTab(tabName) {
  const TabComponent = await tabs[tabName]();
  renderTab(TabComponent.default);
}

Feature Flags

if (features.newDashboard) {
  const { NewDashboard } = await import('./NewDashboard');
  render(NewDashboard);
} else {
  const { OldDashboard } = await import('./OldDashboard');
  render(OldDashboard);
}