Skip to main content

Code Splitting and Lazy Loading in JavaScript

Created: May 8, 2026 Larry Qu 6 min read

Code splitting and lazy loading are essential for optimizing application performance. This article covers techniques for reducing initial bundle size and loading code on-demand.

Introduction

Large JavaScript bundles cause:

  • Slow initial page load
  • High bandwidth usage
  • Poor performance on slow networks
  • Wasted resources on unused code

Code splitting helps you:

  • Reduce initial bundle size
  • Load code on-demand
  • Improve Time to Interactive (TTI)
  • Optimize for different user scenarios

Dynamic Imports

Basic Dynamic Import

// Static import (loaded immediately)
import { heavyModule } from './heavy-module.js';

// Dynamic import (loaded on-demand)
import('./heavy-module.js').then(module => {
  module.doSomething();
});
// Using async/await
async function loadModule() {
  const module = await import('./heavy-module.js');
  module.doSomething();
}

// Call when needed
loadModule();

Conditional Imports

// Load different modules based on conditions
async function loadModule(type) {
  let module;
  
  if (type === 'advanced') {
    module = await import('./advanced-features.js');
  } else {
    module = await import('./basic-features.js');
  }
  
  return module;
}

// Usage
const features = await loadModule('advanced');
// Feature detection
async function loadPolyfill() {
  if (!window.Promise) {
    await import('./promise-polyfill.js');
  }
}

loadPolyfill();

Error Handling

// Handle import errors
async function safeImport(modulePath) {
  try {
    return await import(modulePath);
  } catch (error) {
    console.error(`Failed to load ${modulePath}:`, error);
    // Load fallback
    return await import('./fallback-module.js');
  }
}

// Usage
const module = await safeImport('./optional-feature.js');

Route-Based Code Splitting

React Router Example

// ❌ Bad: All routes loaded upfront
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/contact', component: Contact }
];
// ✅ Good: Route-based code splitting
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/contact', component: Contact }
];

// Wrap routes with Suspense
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        {routes.map(route => (
          <Route key={route.path} path={route.path} element={<route.component />} />
        ))}
      </Routes>
    </Suspense>
  );
}

Vue Router Example

// ✅ Good: Vue route-based code splitting
const routes = [
  {
    path: '/',
    component: () => import('./pages/Home.vue')
  },
  {
    path: '/about',
    component: () => import('./pages/About.vue')
  },
  {
    path: '/contact',
    component: () => import('./pages/Contact.vue')
  }
];

Component-Based Code Splitting

React Lazy Components

// ✅ Good: Lazy load components
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}
// Practical example: Modal component
import { lazy, Suspense, useState } from 'react';

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

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

  return (
    <>
      <button onClick={() => setShowModal(true)}>Open Modal</button>
      
      {showModal && (
        <Suspense fallback={<div>Loading modal...</div>}>
          <Modal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </>
  );
}

Webpack Code Splitting

Entry Points

// webpack.config.js
module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js',
    vendor: './src/vendor.js'
  },
  output: {
    filename: '[name].bundle.js'
  }
};

// Generates:
// - main.bundle.js
// - admin.bundle.js
// - vendor.bundle.js

SplitChunksPlugin

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // Vendor libraries
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        // Common code used in multiple chunks
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
};

Dynamic Imports with Webpack

// Webpack automatically creates chunks for dynamic imports
async function loadFeature() {
  const module = await import(/* webpackChunkName: "feature" */ './feature.js');
  return module;
}

// Generates: feature.bundle.js

Lazy Loading Strategies

Intersection Observer for Images

// ✅ Good: Lazy load images
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.add('loaded');
      imageObserver.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

// HTML:
// <img data-src="image.jpg" src="placeholder.jpg" />

Lazy Load Heavy Libraries

// ✅ Good: Load heavy libraries on-demand
class ChartManager {
  async renderChart(data) {
    // Load chart library only when needed
    const Chart = (await import('chart.js')).default;
    
    const ctx = document.getElementById('chart').getContext('2d');
    new Chart(ctx, {
      type: 'bar',
      data: data
    });
  }
}

// Usage
const manager = new ChartManager();
manager.renderChart(data); // Chart.js loaded on first use

Prefetching and Preloading

// Prefetch: Load resource in background (low priority)
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/next-page.js';
document.head.appendChild(link);

// Preload: Load resource with high priority
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.as = 'script';
preloadLink.href = '/critical-module.js';
document.head.appendChild(preloadLink);
// Webpack magic comments
// Prefetch
import(/* webpackPrefetch: true */ './next-page.js');

// Preload
import(/* webpackPreload: true */ './critical-module.js');

Practical Patterns

Progressive Enhancement

// ✅ Good: Load features progressively
class FeatureManager {
  constructor() {
    this.features = new Map();
  }

  async loadFeature(name) {
    if (this.features.has(name)) {
      return this.features.get(name);
    }

    try {
      const module = await import(`./features/${name}.js`);
      this.features.set(name, module);
      return module;
    } catch (error) {
      console.error(`Failed to load feature ${name}:`, error);
      return null;
    }
  }

  async enableFeature(name) {
    const feature = await this.loadFeature(name);
    if (feature) {
      feature.enable();
    }
  }
}

// Usage
const manager = new FeatureManager();
manager.enableFeature('advanced-search');

Bundle Analysis

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html'
    })
  ]
};

// Run: webpack --mode production
// View: bundle-report.html

Monitoring Bundle Size

// Check bundle size in CI/CD
const fs = require('fs');
const path = require('path');

function checkBundleSize() {
  const bundlePath = path.join(__dirname, 'dist/main.bundle.js');
  const stats = fs.statSync(bundlePath);
  const sizeInKB = stats.size / 1024;
  
  const maxSize = 250; // KB
  
  if (sizeInKB > maxSize) {
    console.error(`Bundle size ${sizeInKB.toFixed(2)}KB exceeds limit ${maxSize}KB`);
    process.exit(1);
  }
  
  console.log(`Bundle size: ${sizeInKB.toFixed(2)}KB (limit: ${maxSize}KB)`);
}

checkBundleSize();

Performance Optimization

Measuring Impact

// Measure code splitting impact
class PerformanceMonitor {
  measureImport(modulePath) {
    const startTime = performance.now();
    
    return import(modulePath).then(module => {
      const duration = performance.now() - startTime;
      console.log(`Loaded ${modulePath} in ${duration.toFixed(2)}ms`);
      return module;
    });
  }
}

// Usage
const monitor = new PerformanceMonitor();
monitor.measureImport('./heavy-module.js');

Caching Strategies

// ✅ Good: Cache loaded modules
class ModuleCache {
  constructor() {
    this.cache = new Map();
  }

  async load(modulePath) {
    if (this.cache.has(modulePath)) {
      return this.cache.get(modulePath);
    }

    const module = await import(modulePath);
    this.cache.set(modulePath, module);
    return module;
  }

  clear() {
    this.cache.clear();
  }
}

// Usage
const cache = new ModuleCache();
const module1 = await cache.load('./module.js'); // Loaded
const module2 = await cache.load('./module.js'); // Cached

Best Practices

  1. Split by route:
    // ✅ Good
    const Home = lazy(() => import('./pages/Home'));
    ```javascript
    
  2. Split by feature:
    // ✅ Good
    const AdvancedSearch = lazy(() => import('./features/AdvancedSearch'));
    ```javascript
    
  3. Prefetch next routes:
    // ✅ Good
    import(/* webpackPrefetch: true */ './next-page.js');
    ```javascript
    
  4. Monitor bundle size:
    // ✅ Good
    checkBundleSize(); // In CI/CD
    ```javascript
    
  5. Use Suspense for loading states:
    // ✅ Good
    <Suspense fallback={<Loading />}>
      <LazyComponent />
    </Suspense>
    ```javascript
    

Common Mistakes

  1. Not splitting at all:
    // ❌ Bad - single large bundle
    // ✅ Good - split by route/feature
    ```javascript
    
  2. Over-splitting:
    // ❌ Bad - too many small chunks
    // ✅ Good - balanced chunk sizes
    ```javascript
    
  3. Not handling loading states:
    // ❌ Bad
    const Component = lazy(() => import('./Component'));
    
    // ✅ Good
    <Suspense fallback={<Loading />}>
      <Component />
    </Suspense>
    ```javascript
    
  4. Ignoring bundle analysis:
    // ❌ Bad - don't know what's in bundle
    // ✅ Good - analyze with BundleAnalyzerPlugin
    

Summary

Code splitting and lazy loading are essential for performance. Key takeaways:

  • Use dynamic imports for on-demand loading
  • Split by route and feature
  • Use Intersection Observer for lazy loading
  • Monitor bundle size
  • Implement proper loading states
  • Cache loaded modules
  • Analyze bundle composition
  • Prefetch/preload critical resources

Next Steps

Resources

Comments

Share this article

Scan to read on mobile