Saved
Frontend Pattern

Code Splitting

Break application code into smaller bundles loaded on demand to reduce initial load time.

Difficulty Advanced

By Den Odell

Code Splitting

Problem

Applications ship a single massive JavaScript bundle containing all routes, features, and dependencies, forcing users to download hundreds of kilobytes or even megabytes before the first pixel renders. Users landing on the homepage must wait for the entire admin dashboard, checkout flow, analytics code, rich text editor, charting libraries, and every other feature to download and parse before they can interact with anything. First contentful paint is delayed by code that most users will never execute, and time-to-interactive suffers as the JavaScript engine parses and compiles functions for features not present on the current page. Mobile users on slow 3G connections spend seconds staring at blank screens while unnecessary code downloads, and even returning visitors with cached code still pay the parsing and execution cost for features they’re not using, while search engine crawlers see poor performance metrics that hurt rankings.

Solution

Split your application at logical boundaries like routes, features, or heavy components using dynamic import() statements that create separate JavaScript bundles. The bundler (Webpack, Rollup, Vite, etc.) analyzes these dynamic imports and automatically generates separate chunk files that load on demand rather than at initial page load. This shrinks the initial bundle to just what’s required for the first page view, dramatically improving time-to-interactive. Route-based splitting loads each page’s code only when users navigate to it. Component-based splitting defers heavy interactive widgets until they’re rendered. Feature flags can gate entire code paths behind dynamic imports so experimental or premium features don’t bloat the bundle for all users. Vendor code (third-party dependencies) can be split into a separate chunk that caches independently from application code.

Example

This example demonstrates using dynamic imports to split code by route, loading the Dashboard component only when users navigate to that route.

Route-Based Splitting with React

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

// Dynamic imports create separate bundles
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          {/* These components load on-demand when routes are accessed */}
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/analytics" element={<Analytics />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Component-Level Splitting

import { lazy, Suspense } from 'react';

// Heavy chart library only loads when Chart component renders
const Chart = lazy(() => import('./components/Chart'));

function Dashboard({ showChart }) {
  return (
    <div>
      <h1>Dashboard</h1>
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <Chart data={data} />
        </Suspense>
      )}
    </div>
  );
}

Vue Router Code Splitting

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/dashboard',
      // Dynamic import creates separate chunk
      component: () => import('./views/Dashboard.vue')
    },
    {
      path: '/analytics',
      component: () => import('./views/Analytics.vue')
    },
    {
      path: '/settings',
      component: () => import('./views/Settings.vue')
    }
  ]
});

Named Chunks with Webpack

// Webpack magic comments control chunk naming and loading
const Dashboard = () => import(
  /* webpackChunkName: "dashboard" */
  /* webpackPrefetch: true */
  './pages/Dashboard'
);

const HeavyEditor = () => import(
  /* webpackChunkName: "editor" */
  /* webpackPreload: true */
  './components/RichTextEditor'
);

// Generates: dashboard.[hash].js, editor.[hash].js

Conditional Loading Based on Feature Flags

async function loadFeature(featureName) {
  if (featureName === 'advanced-analytics') {
    // Only load this code if feature flag is enabled
    const module = await import('./features/AdvancedAnalytics');
    return module.default;
  }
  
  if (featureName === 'admin-panel') {
    const module = await import('./features/AdminPanel');
    return module.default;
  }
  
  return null;
}

// Usage
const AnalyticsComponent = await loadFeature('advanced-analytics');

Vendor Code Splitting

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        // Split all node_modules into separate vendor chunk
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
        },
        // Split common code used by multiple entry points
        common: {
          minChunks: 2,
          name: 'common',
          chunks: 'all'
        }
      }
    }
  }
};

Vite Code Splitting Configuration

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Group React-related code
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          // Group chart libraries
          'charts': ['chart.js', 'd3'],
          // Group UI component library
          'ui-lib': ['@mui/material']
        }
      }
    }
  }
};

Dynamic Import with Error Handling

async function loadDashboard() {
  try {
    const module = await import('./pages/Dashboard');
    return module.default;
  } catch (error) {
    console.error('Failed to load dashboard:', error);
    // Fallback to simple version or show error
    return ErrorComponent;
  }
}

Preloading Critical Chunks

// Preload dashboard chunk on homepage hover
document.querySelector('#dashboard-link').addEventListener('mouseenter', () => {
  // Start downloading before user clicks
  import('./pages/Dashboard');
});

// Or use link preload in HTML
// <link rel="preload" href="/dashboard.chunk.js" as="script">

Svelte Code Splitting

<script>
  // Dynamic import in Svelte
  let DashboardComponent;
  
  async function loadDashboard() {
    const module = await import('./Dashboard.svelte');
    DashboardComponent = module.default;
  }
  
  // Load when route becomes active
  $: if ($page.route.id === '/dashboard') {
    loadDashboard();
  }
</script>

{#if DashboardComponent}
  <svelte:component this={DashboardComponent} />
{:else}
  <LoadingSpinner />
{/if}

Measuring Split Effectiveness

// Track bundle sizes in build output
// Webpack outputs chunk sizes after build:
// main.js         120 KB
// dashboard.js     85 KB
// analytics.js     45 KB
// vendor.js       180 KB

// Monitor in production
performance.measure('chunk-load', 'chunk-start', 'chunk-end');
const measure = performance.getEntriesByName('chunk-load')[0];
console.log(`Chunk loaded in ${measure.duration}ms`);

Benefits

  • Dramatically reduces initial bundle size and improves time-to-interactive by deferring non-critical code until it’s actually needed.
  • Loads code only when needed, saving bandwidth for features users never access, particularly valuable for mobile users on metered connections.
  • Improves perceived performance by showing content faster - users can interact with the page while additional chunks load in the background.
  • Especially beneficial for mobile users on slow networks where every kilobyte matters for performance and data costs.
  • Enables progressive loading of application features, creating a responsive experience even on constrained devices.
  • Vendor chunks can be cached independently from application code, so users don’t re-download third-party libraries after application updates.
  • Improves Core Web Vitals metrics (FCP, LCP, TTI) that affect search engine rankings and user satisfaction.

Tradeoffs

  • Introduces loading states and delays when navigating to new routes or rendering split components - users may experience brief loading spinners or skeleton screens.
  • Can create disruptive user experience if not paired with proper loading UI - blank screens or layout shifts during chunk loading hurt usability.
  • Adds complexity to build configuration and debugging - source maps span multiple files, error stack traces cross chunk boundaries, and build tools require configuration to split effectively.
  • May create more HTTP requests which can be slower on HTTP/1.1 where request parallelism is limited - though HTTP/2 multiplexing mitigates this, not all environments support it.
  • Requires careful splitting strategy to balance bundle size and request count - too much splitting creates waterfall loading, too little splitting defeats the purpose.
  • Preloading or prefetching strategies add additional complexity - determining which chunks to preload requires understanding user navigation patterns and weighing the bandwidth cost against potential benefit.
  • Dynamic imports return promises that must be handled - error cases where chunks fail to load due to network issues require explicit error handling and fallback UI.
  • Cache invalidation becomes more complex - each chunk has its own hash in the filename, and coordinating cache expiration across multiple chunks requires careful configuration.
  • Development experience degrades with hot module replacement - changing shared code may require reloading multiple chunks, slowing down the development feedback loop.
  • Over-splitting can create diminishing returns - very small chunks add overhead from additional HTTP requests and chunk loading boilerplate that outweighs the benefit of splitting.
  • Debugging production issues requires understanding which chunks loaded and in what order - browser DevTools network waterfalls become essential for diagnosing loading problems.
  • Some bundlers handle dynamic imports differently - Webpack, Rollup, and Vite have different magic comment syntaxes and behavior for controlling chunk names and loading strategies.
  • Shared dependencies between chunks require careful configuration to avoid duplication - without proper splitChunks configuration, the same library may be bundled into multiple chunks.
Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.