Skip to main content
โšก Calmops

Code Splitting and Lazy Loading in JavaScript

Code Splitting and Lazy Loading in JavaScript

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'));
    
  2. Split by feature:

    // โœ… Good
    const AdvancedSearch = lazy(() => import('./features/AdvancedSearch'));
    
  3. Prefetch next routes:

    // โœ… Good
    import(/* webpackPrefetch: true */ './next-page.js');
    
  4. Monitor bundle size:

    // โœ… Good
    checkBundleSize(); // In CI/CD
    
  5. Use Suspense for loading states:

    // โœ… Good
    <Suspense fallback={<Loading />}>
      <LazyComponent />
    </Suspense>
    

Common Mistakes

  1. Not splitting at all:

    // โŒ Bad - single large bundle
    // โœ… Good - split by route/feature
    
  2. Over-splitting:

    // โŒ Bad - too many small chunks
    // โœ… Good - balanced chunk sizes
    
  3. Not handling loading states:

    // โŒ Bad
    const Component = lazy(() => import('./Component'));
    
    // โœ… Good
    <Suspense fallback={<Loading />}>
      <Component />
    </Suspense>
    
  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

Comments