Skip to content
Go back

Optimizing React App Performance with Code Splitting

Optimizing React App Performance with Code Splitting

Introduction

Code splitting reduces initial bundle size and improves loading performance by loading code on-demand. This guide covers various splitting strategies in React.

Prerequisites

Step 1: Component-Level Code Splitting

Create components/LazyComponents.tsx:

import { lazy, Suspense } from 'react';

// Lazy load components
const HeavyChart = lazy(() => import('./HeavyChart'));
const DataTable = lazy(() => import('./DataTable'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));

// Loading fallback component
const LoadingSpinner = () => (
  <div className="flex items-center justify-center p-8">
    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
    <span className="ml-2">Loading component...</span>
  </div>
);

export default function LazyComponents() {
  return (
    <div className="space-y-8">
      <div>
        <h2 className="text-xl font-semibold mb-4">Heavy Chart Component</h2>
        <Suspense fallback={<LoadingSpinner />}>
          <HeavyChart /> {/* Only loads when rendered */}
        </Suspense>
      </div>

      <div>
        <h2 className="text-xl font-semibold mb-4">Data Table</h2>
        <Suspense fallback={<LoadingSpinner />}>
          <DataTable />
        </Suspense>
      </div>

      <div>
        <h2 className="text-xl font-semibold mb-4">Video Player</h2>
        <Suspense fallback={<LoadingSpinner />}>
          <VideoPlayer />
        </Suspense>
      </div>
    </div>
  );
}

Step 2: Route-Based Code Splitting

Create router/AppRouter.tsx:

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

// Lazy load route components
const Home = lazy(() => import('../pages/Home'));
const Dashboard = lazy(() => import('../pages/Dashboard'));
const Profile = lazy(() => import('../pages/Profile'));
const Settings = lazy(() => import('../pages/Settings'));

// Route loading component
const RouteLoader = () => (
  <div className="min-h-screen flex items-center justify-center">
    <div className="text-center">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
      <p className="text-gray-600">Loading page...</p>
    </div>
  </div>
);

export default function AppRouter() {
  return (
    <BrowserRouter>
      <Suspense fallback={<RouteLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Step 3: Dynamic Import with Conditions

Create components/ConditionalLoader.tsx:

import { useState, Suspense } from 'react';

interface DynamicComponentProps {
  type: 'chart' | 'table' | 'calendar';
}

function DynamicComponent({ type }: DynamicComponentProps) {
  // Dynamic import based on type
  const [Component, setComponent] = useState<React.ComponentType | null>(null);

  const loadComponent = async () => {
    let ImportedComponent;

    switch (type) {
      case 'chart':
        ImportedComponent = await import('./ChartComponent');
        break;
      case 'table':
        ImportedComponent = await import('./TableComponent');
        break;
      case 'calendar':
        ImportedComponent = await import('./CalendarComponent');
        break;
      default:
        return;
    }

    setComponent(() => ImportedComponent.default);
  };

  if (!Component) {
    return (
      <button
        onClick={loadComponent}
        className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
      >
        Load {type.charAt(0).toUpperCase() + type.slice(1)} Component
      </button>
    );
  }

  return (
    <Suspense fallback={<div>Loading {type}...</div>}>
      <Component />
    </Suspense>
  );
}

export default function ConditionalLoader() {
  return (
    <div className="space-y-6 p-6">
      <h2 className="text-2xl font-bold">Conditional Component Loading</h2>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <div className="border rounded-lg p-4">
          <h3 className="font-semibold mb-4">Chart Component</h3>
          <DynamicComponent type="chart" />
        </div>

        <div className="border rounded-lg p-4">
          <h3 className="font-semibold mb-4">Table Component</h3>
          <DynamicComponent type="table" />
        </div>

        <div className="border rounded-lg p-4">
          <h3 className="font-semibold mb-4">Calendar Component</h3>
          <DynamicComponent type="calendar" />
        </div>
      </div>
    </div>
  );
}

Step 4: Library Code Splitting

Create utils/dynamicImports.ts:

// Split large libraries into separate chunks
export const loadMoment = async () => {
  const moment = await import("moment");
  return moment.default;
};

export const loadLodash = async () => {
  const _ = await import("lodash");
  return _;
};

export const loadChartJS = async () => {
  const { Chart, registerables } = await import("chart.js");
  Chart.register(...registerables);
  return Chart;
};

// Utility function for conditional library loading
export const loadLibraryOnDemand = async <T>(
  libraryName: string,
  loader: () => Promise<T>
): Promise<T> => {
  console.log(`Loading ${libraryName}...`);
  const startTime = performance.now();

  try {
    const library = await loader();
    const loadTime = performance.now() - startTime;
    console.log(`${libraryName} loaded in ${loadTime.toFixed(2)}ms`);
    return library;
  } catch (error) {
    console.error(`Failed to load ${libraryName}:`, error);
    throw error;
  }
};

Step 5: Advanced Preloading Strategies

Create hooks/usePreload.ts:

import { useEffect, useState } from 'react';

interface PreloadOptions {
  delay?: number;
  condition?: boolean;
}

export function usePreload(
  importFn: () => Promise<any>,
  { delay = 0, condition = true }: PreloadOptions = {}
) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    if (!condition) return;

    const preload = async () => {
      try {
        await new Promise(resolve => setTimeout(resolve, delay));
        await importFn();
        setIsLoaded(true);
      } catch (err) {
        setError(err instanceof Error ? err : new Error('Preload failed'));
      }
    };

    preload();
  }, [importFn, delay, condition]);

  return { isLoaded, error };
}

// Usage component
export function PreloadingExample() {
  const [showChart, setShowChart] = useState(false);

  // Preload chart component after 2 seconds
  const { isLoaded } = usePreload(
    () => import('../components/HeavyChart'),
    { delay: 2000 }
  );

  return (
    <div className="p-6">
      <div className="mb-4">
        <span className={`inline-block w-3 h-3 rounded-full mr-2 ${
          isLoaded ? 'bg-green-500' : 'bg-yellow-500'
        }`}></span>
        Chart component {isLoaded ? 'preloaded' : 'loading...'}
      </div>

      <button
        onClick={() => setShowChart(!showChart)}
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        {showChart ? 'Hide' : 'Show'} Chart
      </button>

      {showChart && (
        <div className="mt-4">
          <Suspense fallback={<div>Loading chart...</div>}>
            <LazyChart />
          </Suspense>
        </div>
      )}
    </div>
  );
}

const LazyChart = lazy(() => import('../components/HeavyChart'));

Step 6: Bundle Analysis and Monitoring

Create utils/bundleAnalytics.ts:

// Monitor chunk loading performance
export class ChunkLoadMonitor {
  private static loadTimes = new Map<string, number>();

  static startLoading(chunkName: string) {
    this.loadTimes.set(chunkName, performance.now());
  }

  static endLoading(chunkName: string) {
    const startTime = this.loadTimes.get(chunkName);
    if (startTime) {
      const loadTime = performance.now() - startTime;
      console.log(`Chunk "${chunkName}" loaded in ${loadTime.toFixed(2)}ms`);

      // Send to analytics
      if (typeof window !== "undefined" && "gtag" in window) {
        (window as any).gtag("event", "chunk_load", {
          chunk_name: chunkName,
          load_time: Math.round(loadTime),
        });
      }

      this.loadTimes.delete(chunkName);
    }
  }

  static getStats() {
    return {
      pendingChunks: Array.from(this.loadTimes.keys()),
      pendingCount: this.loadTimes.size,
    };
  }
}

// Enhanced lazy loading with monitoring
export function createMonitoredLazy<T extends React.ComponentType<any>>(
  importFn: () => Promise<{ default: T }>,
  chunkName: string
) {
  return lazy(async () => {
    ChunkLoadMonitor.startLoading(chunkName);
    try {
      const module = await importFn();
      ChunkLoadMonitor.endLoading(chunkName);
      return module;
    } catch (error) {
      ChunkLoadMonitor.endLoading(chunkName);
      throw error;
    }
  });
}

Step 7: Production Bundle Configuration

Create webpack.config.js (if using custom Webpack):

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          chunks: "all",
        },
        common: {
          name: "common",
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

For Vite, update vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ["react", "react-dom"],
          router: ["react-router-dom"],
          ui: ["framer-motion", "@headlessui/react"],
        },
      },
    },
  },
});

Summary

Code splitting with React.lazy(), Suspense, and dynamic imports significantly improves initial load performance. Combine route-based, component-based, and library splitting for optimal results.


Share this post on:

Previous Post
Creating a PWA with React + Vite
Next Post
Using Framer Motion for Complex React Animations