Mahamudul Hasan Rubel
HomeBlogCoursesAboutProjectsSkillsExperiencePhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • Blog
  • Courses
  • About
  • Projects
  • Skills
  • Experience
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

Subscribe to the newsletter

Get new articles and course lessons delivered to your inbox. No spam, unsubscribe anytime.

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
Lesson 25 of the Advanced React: Performance, Architecture & Patterns course
ReactJune 28, 20264 min read

Route-level Code Splitting: Shrinking Bundles in React

Master Code Splitting in React using dynamic imports and Suspense. Learn how to architect your app for faster initial loads and smaller bundle sizes.

ReactPerformanceCode SplittingWebpackVitejavascriptfrontend

Previously in this course, we discussed Establishing Performance Budgets to keep our application's footprint in check. While setting budgets prevents bloat, today we tackle the primary architectural strategy for staying within those limits: Route-level Code Splitting.

By default, bundlers like Webpack or Vite bundle your entire application into a single JavaScript file. As your app grows, this "main bundle" becomes a bottleneck, forcing users to download code for pages they haven't even visited yet. Route-level splitting allows us to break this monolith into smaller, lazily-loaded chunks.

The Mechanics of Dynamic Imports

At the heart of code splitting is the dynamic import() syntax. Unlike static import statements that are hoisted to the top of your file and included in the initial bundle, import() returns a promise that resolves to the module only when invoked.

In React, we use React.lazy to make this seamless. It lets you render a dynamic import as a regular component.

JAVASCRIPT
// Instead of: import Dashboard from CE9178">'./Dashboard';
// We use:
const Dashboard = React.lazy(() => import(CE9178">'./Dashboard'));

When React encounters this Dashboard component, it triggers the network request to fetch the corresponding chunk. Because this is asynchronous, we must wrap it in a Suspense boundary to provide a fallback UI while the chunk is in transit.

Configuring Route-Based Splitting

In our project, we are currently importing all pages in our App.js router. We need to refactor this to split our route definitions.

Before: Static Imports

JSX
import { Routes, Route } from CE9178">'react-router-dom';
import Home from CE9178">'./pages/Home';
import Dashboard from CE9178">'./pages/Dashboard';
import Settings from CE9178">'./pages/Settings';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/settings" element={<Settings />} />
    </Routes>
  );
}

After: Dynamic Route Splitting

To optimize, we define our lazy components and wrap the Routes component in Suspense.

JSX
import React, { Suspense, lazy } from CE9178">'react';
import { Routes, Route } from CE9178">'react-router-dom';

const Home = lazy(() => import(CE9178">'./pages/Home'));
const Dashboard = lazy(() => import(CE9178">'./pages/Dashboard'));
const Settings = lazy(() => import(CE9178">'./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading page...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

This simple change instructs your bundler to create separate files for Home, Dashboard, and Settings. The browser will now only download Dashboard.js when the user navigates to /dashboard.

Verifying Bundle Size Improvements

You cannot optimize what you cannot measure. After implementing these changes, you must verify the impact using your bundler's analysis tools.

ToolPurpose
rollup-plugin-visualizerGenerates a treemap of your production bundle.
webpack-bundle-analyzerVisualizes the size of output files in Webpack.
Chrome DevTools (Network)Inspects the waterfall of lazy-loaded chunks.

When you run your build, look for the generated chunks in your dist/ or build/ folder. You should see separate files (e.g., Dashboard-xyz123.js) rather than one massive index.js.

Hands-on Exercise

  1. Open your project's main router file.
  2. Identify the three largest pages in your application.
  3. Convert their imports to React.lazy.
  4. Wrap your Routes or the specific Route elements in Suspense with a loading spinner.
  5. Run your build process and inspect the output directory to verify that the chunks are indeed separated.

Common Pitfalls

  • Suspense Placement: Placing Suspense too high can result in the entire layout "flashing" every time a tiny component loads. Place it as close to the lazy component as possible, or at the route level to maintain a good user experience.
  • Error Boundaries: If a network request fails (e.g., the user is offline or a new deployment invalidated the old chunk), the app will crash. Always wrap your lazy routes in an Error Boundary. We will cover advanced error handling in a future lesson, but for now, ensure your Suspense strategy doesn't mask these failures.
  • Over-splitting: Don't lazy-load every tiny button or icon. The overhead of individual HTTP requests can outweigh the benefits of a smaller bundle. Focus on route-level or heavy feature-level chunks.

Recap

By leveraging React.lazy and Suspense, we effectively decouple our application's entry point from its feature set. This reduces the initial payload, improves Time to Interactive (TTI), and aligns with our performance budgets by ensuring only necessary code is downloaded.

As we continue to build out our architecture, remember that modular directory structures make this splitting strategy significantly easier to maintain.

Up next: We will explore how to take expensive computations off the main thread using Web Workers, further ensuring our UI remains buttery smooth.

Previous lessonServer-Client State SynchronizationNext lesson Offloading Tasks with Web Workers
Back to Blog

Similar Posts

ReactJune 26, 20264 min read

Optimizing Asset Loading: Performance, Lazy Loading, Code Splitting

Master performance optimization in React. Learn how to use lazy loading, code splitting, and image optimization to keep your dashboard fast and efficient.

Read more
ReactJune 28, 20264 min read

Monitoring Production Performance: A Senior Engineer's Guide

Stop guessing why your app feels slow. Learn to integrate monitoring, set actionable performance alerts, and analyze real-world trends in production.

Part of the course

Advanced React: Performance, Architecture & Patterns

advanced · Lesson 25 of 47

  1. 1

    Deep Dive into the Reconciliation Algorithm

    4 min
  2. 2

    Profiling with React DevTools

    3 min
  3. 3

    Establishing Performance Budgets

    3 min
Read more
ReactJune 28, 20264 min read

Advanced Cache Invalidation: Mastering Data Sync in React Query

Learn to master Cache Invalidation in React. Configure precise keys, perform manual mutations, and handle stale-while-revalidate patterns for robust apps.

Read more
  • 4

    Strategic use of React.memo

    3 min
  • 5

    Mastering useCallback and useMemo

    4 min
  • 6

    State Colocation Strategies

    4 min
  • 7

    Optimizing Context Providers

    4 min
  • 8

    Advanced Context Composition

    4 min
  • 9

    Eliminating Prop Drilling

    4 min
  • 10

    Introduction to Concurrent React

    4 min
  • 11

    Non-blocking UI with useTransition

    4 min
  • 12

    Handling Deferred Data with useDeferredValue

    3 min
  • 13

    Mastering Suspense for Data Fetching

    4 min
  • 14

    Streaming Server-Side Rendering

    3 min
  • 15

    Designing Compound Components

    3 min
  • 16

    The Render Props Pattern

    4 min
  • 17

    Implementing Control Props

    4 min
  • 18

    Headless UI Architectures

    3 min
  • 19

    Modular Directory Structures

    3 min
  • 20

    Refactoring Monolithic Components

    3 min
  • 21

    Optimistic UI Updates

    3 min
  • 22

    Advanced Cache Invalidation

    4 min
  • 23

    Handling Race Conditions

    4 min
  • 24

    Server-Client State Synchronization

    3 min
  • 25

    Route-level Code Splitting

    4 min
  • 26

    Offloading Tasks with Web Workers

    3 min
  • 27

    Advanced Error Boundaries

    3 min
  • 28

    Monitoring Production Performance

    4 min
  • 29

    Final Project Audit & Optimization

    Coming soon
  • 30

    Advanced Hook Patterns

    Coming soon
  • 31

    Managing Global State with Zustand/Redux

    Coming soon
  • 32

    Testing Performance-Critical Components

    Coming soon
  • 33

    Static Site Generation (SSG) Patterns

    Coming soon
  • 34

    Internationalization (i18n) Architecture

    Coming soon
  • 35

    Accessibility (a11y) in Advanced Components

    Coming soon
  • 36

    Managing Third-Party Integrations

    Coming soon
  • 37

    Advanced Form Handling

    Coming soon
  • 38

    Using Portals for UI Overlays

    Coming soon
  • 39

    Implementing Virtualized Lists

    Coming soon
  • 40

    Building Design System Primitives

    Coming soon
  • 41

    Managing Large-Scale Data Fetching

    Coming soon
  • 42

    Micro-Frontends with React

    Coming soon
  • 43

    Security Best Practices in React

    Coming soon
  • 44

    Advanced Ref Usage

    Coming soon
  • 45

    Memoization Pitfalls

    Coming soon
  • 46

    Mastering React Patterns for Scalability

    Coming soon
  • 47

    Advanced TypeScript with React

    Coming soon
  • View full course