Skip to content
All essays
WebMarch 27, 202416 min

Advanced Code Splitting Strategies for Modern Web Apps

Master code splitting techniques for React, Vue, and vanilla JS to minimize initial bundle size and improve load performance

Ü
Ümit Uz
Mobile & Full Stack Developer

Code splitting is one of the most powerful optimization techniques for modern web applications. By splitting your code into smaller chunks, you can load only what's needed, when it's needed, dramatically improving initial load times.

Understanding Code Splitting

Code splitting divides your bundle into smaller pieces that can be loaded on demand. This reduces the initial bundle size and improves Time to Interactive (TTI).

Basic Dynamic Imports

javascript
// Basic dynamic import
async function loadModule() {
  const module = await import('./heavy-module.js');
  module.doSomething();
}

// Conditional loading
if (process.env.NODE_ENV === 'development') {
  const devTools = await import('./dev-tools');
  devTools.init();
}

React Code Splitting

Route-Based Splitting

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

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Profile = lazy(() => import('./pages/Profile'));
const Admin = lazy(() => import('./pages/Admin'));

// Loading component
function PageLoader() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <Spinner size="large" />
    </div>
  );
}

// Error boundary for lazy loading
class LazyErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

function App() {
  return (
    <BrowserRouter>
      <LazyErrorBoundary>
        <Suspense fallback={<PageLoader />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/products" element={<Products />} />
            <Route path="/products/:id" element={<ProductDetail />} />
            <Route path="/cart" element={<Cart />} />
            <Route path="/checkout" element={<Checkout />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="/admin" element={<Admin />} />
            <Route path="*" element={<Navigate to="/" replace />} />
          </Routes>
        </Suspense>
      </LazyErrorBoundary>
    </BrowserRouter>
  );
}

Component-Based Splitting

javascript
import { lazy, Suspense, useState } from 'react';

// Lazy load heavy components
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const DataGrid = lazy(() => import('./DataGrid'));
const Chart = lazy(() => import('./Chart'));
const Map = lazy(() => import('./Map'));

function Dashboard() {
  const [showEditor, setShowEditor] = useState(false);
  const [showChart, setShowChart] = useState(false);

  return (
    <div className="dashboard">
      <button onClick={() => setShowEditor(true)}>
        Create Post
      </button>

      {showEditor && (
        <Suspense fallback={<Spinner />}>
          <RichTextEditor />
        </Suspense>
      )}

      <button onClick={() => setShowChart(true)}>
        View Analytics
      </button>

      {showChart && (
        <Suspense fallback={<Spinner />}>
          <Chart data={analyticsData} />
        </Suspense>
      )}
    </div>
  );
}

Named Exports with Code Splitting

javascript
// ✅ Use default export for easier lazy loading
export default function MyComponent() {
  return <div>...</div>;
}

// Usage
const Component = lazy(() => import('./MyComponent'));

// ❌ Named exports require intermediate re-export
export function ComponentA() { return <div>A</div>; }
export function ComponentB() { return <div>B</div>; }

// Re-export as default in component.js
export { ComponentA as default } from './ComponentA';

// Usage
const ComponentA = lazy(() => import('./components/ComponentA'));

Vue.js Code Splitting

Route-Based Splitting

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true }
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

Component Lazy Loading

vue
<template>
  <div>
    <button @click="showChart = true">Show Chart</button>

    <Suspense>
      <template #default>
        <Chart v-if="showChart" :data="chartData" />
      </template>

      <template #fallback>
        <div>Loading chart...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { defineAsyncComponent, ref } from 'vue';

const showChart = ref(false);

const Chart = defineAsyncComponent(() =>
  import('@/components/Chart.vue')
);
</script>

Async Component with Error Handling

javascript
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200, // Show loading after 200ms
  timeout: 10000 // Error after 10s
});

Advanced Splitting Patterns

Prefetching and Preloading

javascript
import { lazy } from 'react';

// Prefetch: Load during browser idle time
const Dashboard = lazy(() =>
  import(/* webpackPrefetch: true */ './Dashboard')
);

// Preload: Load in parallel to parent bundle
const CriticalComponent = lazy(() =>
  import(/* webpackPreload: true */ './CriticalComponent')
);

// Webpack magic comments for better naming
const About = lazy(() =>
  import(/* webpackChunkName: "about-page" */ './About')
);

// Multiple entries in same chunk
const UserSettings = lazy(() =>
  import(/* webpackChunkName: "settings" */ './UserSettings')
);
const AccountSettings = lazy(() =>
  import(/* webpackChunkName: "settings" */ './AccountSettings')
);

Splitting by Features

javascript
// Feature-based splitting
const authFeature = () => import('./features/auth');
const checkoutFeature = () => import('./features/checkout');
const adminFeature = () => import('./features/admin');

// Load entire feature when needed
async function initializeAuth() {
  const { login, logout, isAuthenticated } = await authFeature();
  return { login, logout, isAuthenticated };
}

async function openCheckout() {
  const { Cart, Payment, Shipping } = await checkoutFeature();
  return { Cart, Payment, Shipping };
}

Vendor Splitting

javascript
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // React vendor chunk
        reactVendor: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react-vendor',
          priority: 10,
        },

        // UI library chunk
        uiVendor: {
          test: /[\\/]node_modules[\\/](@mui|@material-ui|framer-motion)[\\/]/,
          name: 'ui-vendor',
          priority: 9,
        },

        // Utility libraries
        utilsVendor: {
          test: /[\\/]node_modules[\\/](lodash|axios|dayjs)[\\/]/,
          name: 'utils-vendor',
          priority: 8,
        },

        // Common code
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

Webpack Code Splitting

Entry Point Splitting

javascript
module.exports = {
  entry: {
    main: './src/main.js',
    admin: './src/admin.js',
    vendor: ['react', 'react-dom', 'lodash'],
  },

  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};

Dynamic Import Syntax

javascript
// Basic dynamic import
import('./modules/myModule').then(module => {
  module.doSomething();
});

// With error handling
import('./modules/myModule')
  .then(module => {
    module.default();
  })
  .catch(error => {
    console.error('Failed to load module:', error);
    // Load fallback
    return import('./modules/fallback');
  });

// Async/await syntax
async function loadComponent() {
  try {
    const { default: Component } = await import('./Component');
    return Component;
  } catch (error) {
    console.error('Load failed:', error);
    return ErrorComponent;
  }
}

Vite Code Splitting

Manual Chunk Splitting

javascript
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React ecosystem
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],

          // UI libraries
          'ui-vendor': ['@mui/material', '@emotion/react', '@emotion/styled'],

          // Utilities
          'utils': ['lodash-es', 'axios', 'dayjs'],

          // Larger libraries
          'editor': ['@monaco-editor/react'],
          'charts': ['recharts', 'd3-scale'],
        },
      },
    },
  },
};

Performance Optimization

Progressive Loading

javascript
import { lazy, Suspense } from 'react';

// Load critical content first
const Hero = lazy(() => import(/* webpackPrefetch: true */ './Hero'));

// Load below-fold content later
const Features = lazy(() => import('./Features'));
const Testimonials = lazy(() => import('./Testimonials'));
const Pricing = lazy(() => import('./Pricing'));

function LandingPage() {
  return (
    <div>
      <Suspense fallback={<HeroSkeleton />}>
        <Hero />
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <Features />
        <Testimonials />
        <Pricing />
      </Suspense>
    </div>
  );
}

Intersection Observer Splitting

javascript
import { lazy, Suspense, useEffect, useState, useRef } from 'react';

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

function LazyOnScroll() {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1 }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={ref}>
      {isVisible ? (
        <Suspense fallback={<Spinner />}>
          <HeavyComponent />
        </Suspense>
      ) : (
        <div style={{ height: '400px' }} />
      )}
    </div>
  );
}

Monitoring and Analysis

Bundle Analysis

javascript
// Track loaded chunks
const chunkTracker = new Set();

window.addEventListener('load', () => {
  performance.getEntriesByType('resource').forEach(resource => {
    if (resource.name.includes('.js')) {
      const chunkName = resource.name.split('/').pop();
      chunkTracker.add(chunkName);
      console.log(`Chunk loaded: ${chunkName}`);
    }
  });

  console.log(`Total chunks loaded: ${chunkTracker.size}`);
});

Best Practices

  1. 1Split by Route: Route-based splitting provides the biggest impact
  2. 2Lazy Load Heavy Components: Load charts, editors, and maps on demand
  3. 3Prefetch Intelligently: Prefetch likely next pages during idle time
  4. 4Vendor Splitting: Separate frequently updated libraries from stable ones
  5. 5Set Loading States: Always provide visual feedback during loading
  6. 6Error Boundaries: Handle lazy loading failures gracefully
  7. 7Measure Impact: Use tools to measure actual performance improvements
  8. 8Avoid Over-Splitting: Too many chunks cause HTTP request overhead

Conclusion

Code splitting is essential for modern web performance. Start with route-based splitting, then progressively lazy-load heavy components and features. Monitor your bundle sizes and loading patterns to continuously optimize your splitting strategy.

Related essays

Next essay
Implementing Passkeys: The Passwordless Future of Authentication