Skip to content

Route-Based Splitting — Splitting Code by Application Routes

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Route. We cover key concepts, practical examples, and best practices to help you master this topic.

Route-based code splitting loads JavaScript chunks per route, ensuring users only download code for the pages they actually visit.

What You'll Learn

By the end of this tutorial, you'll understand how to implement route-based code splitting in React, Vue, and other frameworks, how to configure lazy routes, and how to preload likely-next routes.

Why It Matters

In large SPAs, the initial bundle contains code for every route — even routes the user may never visit. Route-based splitting ensures users only download code for the current route, dramatically reducing initial bundle size.

Real-World Use

An e-commerce SPA has 20 routes (home, category, product, cart, checkout, account, etc.). Route-based splitting reduces the initial bundle from 600KB to 150KB. The checkout page code only loads when the user reaches the cart.

Route Splitting Architecture

graph TD
    A[User visits site] --> B[Load core chunk
router, layout, auth] B --> C[User navigates
to /products] B --> D[Preload likely
next routes] C --> E[Dynamic import
Products chunk] E --> F[Products page
renders] D --> G[Preload: /products/:id
Preload: /cart] style B fill:#27ae60,color:#fff style D fill:#4a90d9,color:#fff style E fill:#e67e22,color:#fff style G fill:#f39c12,color:#fff

React Router Code Splitting

// routes.js — Centralized route configuration with lazy loading
import React, { lazy } from 'react';

// Route-level code splitting
const routes = [
    {
        path: '/',
        component: lazy(() => import('./pages/Home')),
        preload: true, // Preload immediately after initial render
    },
    {
        path: '/products',
        component: lazy(() => import('./pages/Products')),
        preload: ['/products/:id', '/cart'], // Preload these next
    },
    {
        path: '/products/:id',
        component: lazy(() => import('./pages/ProductDetail')),
        preload: ['/products', '/cart'],
    },
    {
        path: '/cart',
        component: lazy(() => import('./pages/Cart')),
        preload: ['/checkout'],
    },
    {
        path: '/checkout',
        component: lazy(() => import('./pages/Checkout')),
        preload: [],
    },
    {
        path: '/account',
        component: lazy(() => import('./pages/Account')),
        preload: ['/account/orders', '/account/settings'],
    },
    {
        path: '/account/orders',
        component: lazy(() => import('./pages/account/Orders')),
    },
    {
        path: '/account/settings',
        component: lazy(() => import('./pages/account/Settings')),
    },
    {
        path: '/admin',
        component: lazy(() => import('./pages/admin/Dashboard')),
        preload: ['/admin/users', '/admin/products'],
    },
    {
        path: '*',
        component: lazy(() => import('./pages/NotFound')),
    },
];

Vue Router Lazy Loading

// router/index.js — Vue Router with lazy routes
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        name: 'Home',
        component: () => import('../views/Home.vue'),
    },
    {
        path: '/products',
        name: 'Products',
        component: () => import('../views/Products.vue'),
        children: [
            {
                path: ':id',
                name: 'ProductDetail',
                component: () => import('../views/ProductDetail.vue'),
            },
        ],
    },
    {
        path: '/cart',
        name: 'Cart',
        component: () => import('../views/Cart.vue'),
    },
    {
        path: '/checkout',
        name: 'Checkout',
        component: () => import('../views/Checkout.vue'),
        // Lazy load nested components
        meta: {
            preloadComponents: [
                () => import('../components/PaymentForm.vue'),
                () => import('../components/OrderSummary.vue'),
            ],
        },
    },
    {
        path: '/admin',
        name: 'Admin',
        component: () => import('../views/admin/AdminLayout.vue'),
        meta: { requiresAuth: true },
        children: [
            {
                path: '',
                component: () => import('../views/admin/Dashboard.vue'),
            },
            {
                path: 'products',
                component: () => import('../views/admin/ProductManager.vue'),
            },
            {
                path: 'users',
                component: () => import('../views/admin/UserManager.vue'),
            },
        ],
    },
];

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

Preloading Strategies

// lib/route-preloader.js — Intelligent route preloading
class RoutePreloader {
    constructor(routes) {
        this.routes = routes;
        this.loadedChunks = new Set();
        this.preloadQueue = [];
    }

    // Preload on idle time
    preloadOnIdle(routePath) {
        if ('requestIdleCallback' in window) {
            requestIdleCallback(() => {
                this.preloadRoutes(routePath);
            }, { timeout: 2000 });
        } else {
            setTimeout(() => this.preloadRoutes(routePath), 1000);
        }
    }

    // Preload routes likely to be visited next
    preloadRoutes(currentPath) {
        const route = this.routes.find(r => r.path === currentPath);
        if (!route || !route.preload) return;

        route.preload.forEach(preloadPath => {
            const targetRoute = this.routes.find(r => r.path === preloadPath);
            if (targetRoute && !this.loadedChunks.has(preloadPath)) {
                this.preloadQueue.push(targetRoute);
                this.processQueue();
            }
        });
    }

    async processQueue() {
        while (this.preloadQueue.length > 0) {
            const route = this.preloadQueue.shift();
            try {
                const module = await route.component();
                this.loadedChunks.add(route.path);
                console.log(`Preloaded: ${route.path}`);
            } catch (err) {
                console.warn(`Failed to preload ${route.path}:`, err);
            }
        }
    }

    // Preload on hover
    preloadOnHover(linkPath) {
        const route = this.routes.find(r => r.path === linkPath);
        if (route && !this.loadedChunks.has(linkPath)) {
            route.component();
            this.loadedChunks.add(linkPath);
        }
    }

    // Stats
    getStats() {
        return {
            totalRoutes: this.routes.length,
            preloaded: this.loadedChunks.size,
            pending: this.preloadQueue.length,
            preloadedRoutes: [...this.loadedChunks]
        };
    }
}

const preloader = new RoutePreloader(routes);

// Usage in navigation component
function NavLink({ to, children }) {
    return (
        <a
            href={to}
            onMouseEnter={() => preloader.preloadOnHover(to)}
            onClick={(e) => {
                e.preventDefault();
                preloader.preloadOnIdle(to);
                navigate(to);
            }}
        >
            {children}
        </a>
    );
}

Loading States Per Route

// components/RouteLoading.js — Per-route loading states
const routeLoadingStates = {
    // Custom loading for each route type
    default: {
        component: () => <div className="generic-loader" />,
        delay: 200,
    },
    products: {
        component: () => (
            <div className="product-grid-skeleton">
                {[...Array(8)].map((_, i) => (
                    <div key={i} className="product-card-skeleton">
                        <div className="skeleton-image" />
                        <div className="skeleton-title" />
                        <div className="skeleton-price" />
                    </div>
                ))}
            </div>
        ),
        delay: 100,
    },
    checkout: {
        component: () => (
            <div className="checkout-skeleton">
                <div className="skeleton-form">
                    <div className="skeleton-input" />
                    <div className="skeleton-input" />
                    <div className="skeleton-input" />
                </div>
                <div className="skeleton-summary" />
            </div>
        ),
        delay: 300,
    },
    admin: {
        component: () => <AdminSkeleton />,
        delay: 0, // Admin users expect fast loading
    },
};

function RouteLoading({ routeName }) {
    const config = routeLoadingStates[routeName] || routeLoadingStates.default;
    const [show, setShow] = useState(false);

    useEffect(() => {
        const timer = setTimeout(() => setShow(true), config.delay);
        return () => clearTimeout(timer);
    }, [config.delay]);

    if (!show) return null;

    return <config.component />;
}

Common Mistakes

  1. Splitting every route equally. Some routes (login, home) are visited by everyone. Others (admin, settings) are rarely visited. Split aggressively for rare routes, less for common ones.
  2. Not preloading likely-next routes. After loading the home page, preload the products route. After adding to cart, preload checkout. Preloading makes navigation feel instant.
  3. Over-splitting admin sections. Admin users accept slightly larger initial loads for feature completeness. Group admin routes into reasonable chunks.
  4. Forgetting shared dependencies. If Home and Products both use the same ProductCard component, it gets duplicated in both chunks. Extract shared components into a common chunk.
  5. Not testing with navigation timing. Route changes should feel instant (under 200ms chunk load). Measure navigation timing and adjust splitting Strategy.

Practice Questions

  1. How does route-based code splitting reduce initial bundle size?
  2. How do you implement route splitting in Vue Router vs React Router?
  3. What is route preloading and when should you use it?
  4. How do you handle shared dependencies between routes?
  5. How do you measure navigation timing for split routes?

Challenge: Build a 15-route SPA with route-based code splitting: configure lazy routes in React Router, implement preloading for likely-next routes, add route-specific loading skeletons, measure chunk sizes per route, and set up bundle analysis.

FAQ

How many routes should be in a single chunk?

2-5 related routes per chunk is ideal. Group admin routes, public routes, and auth-related routes into logical groups. Don't split every single route.

Does route splitting affect SEO?

For SPAs, yes. Search engines may not execute JavaScript for lazy routes. Use SSR or SSG for critical routes. Next.js handles this with per-page code splitting automatically.

Can I use route splitting with Next.js?

Next.js does this automatically. Each page in the pages/ directory is a separate chunk. You don't need React.lazy — Next.js handles code splitting internally.

How do I handle 404 routes with splitting?

The 404 page should be in the core chunk or preloaded immediately. Users landing on a 404 should see it instantly without waiting for a chunk load.

Should I split by route or by feature?

Both. Use route splitting as the primary strategy. Then use component-level splitting within a route for heavy features (charts, editors, maps).

Mini Project

Build a 12-route SPA with intelligent route splitting: implement lazy loading for all routes, group admin routes into a single chunk, add preload-on-hover for navigation, create route-specific loading skeletons, measure and display chunk sizes, and benchmark navigation timing.

What's Next

You've mastered route-based splitting. Now learn about Component-Level Splitting for lazy loading individual components within a page.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro