Making Your React App Feel Instant: A Deep Dive into Route Optimization

Lazy loading with React Router v7 (Data Mode) = faster initial loads. Async lazy() on your route definitions handles the dynamic imports. Code for each route is chunked and fetched only when navigated to. This results in better perceived performance and less initial JS. Happy users, happy app. import { createBrowserRouter, type RouteObject } from 'react-router-dom'; enum AppRoute { ROOT = '/', HOME = '/', ABOUT = '/about' } export const AppRoutes = { ...AppRoute } as const; const routes: RouteObject[] = [ { path: AppRoute.ROOT, async lazy() { const c = await import('./components/Layout'); return { Component: c.default }; }, children: [ { index: true, async lazy() { const c = await import('./pages/Home'); return { Component: c.default }; } }, { path: AppRoute.ABOUT, async lazy() { const c = await import('./pages/About'); return { Component: c.default }; } }, ], }, ]; const router = createBrowserRouter(routes); export default router; Lazy loading makes your app load fast initially, but what if the network fails when fetching a page's code? Users might just see an error. As developers, we shouldn't just accept defeat. Could we build in a way to automatically retry loading those chunks? This could make our apps more resilient, especially for users with spotty internet. Imagine a "Retry" button or an automatic background attempt to fetch the missing code. It's about making the experience smoother even when the network isn't perfect. Here's a function called retryLazy designed to enhance the resilience of your lazy-loaded components. export default function retryLazy( loader: () => Promise, retries = 3, delay = 1000 ): () => Promise { return async () => { for (let i = 0; i setTimeout(res, delay)); } } throw new Error('Unexpected retryLazy failure'); }; } This retryLazy function takes a dynamic import loader and attempts to execute it up to a specified number of times with a set delay between retries. This helps gracefully handle temporary network issues when loading your lazy-loaded React components. Now, let's see how to integrate this retryLazy function into your React Router v7 setup. Instead of directly using the lazy function provided by React, you can wrap your dynamic imports with our retryLazy function. This applies the retry mechanism to each of your lazily loaded route components. import { createBrowserRouter, type RouteObject } from 'react-router-dom'; import retryLazy from './utils/retryLazy'; enum AppRoute { ROOT = '/', HOME = '/', ABOUT = '/about', SERVICES = '/services', BLOG = '/blog', CONTACT = '/contact' } export const AppRoutes = { ...AppRoute, } as const; const routes: RouteObject[] = [ { path: AppRoute.ROOT, lazy: retryLazy(() => import('./components/Layout')), children: [ { index: true, lazy: retryLazy(() => import('./pages/Home')), }, { path: AppRoute.ABOUT, lazy: retryLazy(() => import('./pages/About')), }, { path: AppRoute.SERVICES, lazy: retryLazy(() => import('./pages/Services')), }, { path: AppRoute.BLOG, lazy: retryLazy(() => import('./pages/Blog')), }, { path: AppRoute.CONTACT, lazy: retryLazy(() => import('./pages/Contact')), }, ], }, ]; const router = createBrowserRouter(routes); export default router; Alright, welcome to the next level of optimizing your React Router experience! We've tackled the initial load times with lazy loading, making that first impression lightning fast. But what happens when users navigate to a page packed with code or hefty dependencies? Even with lazy loading, there can still be a noticeable delay as those larger chunks are fetched and processed. However, we don't have to accept this as the final behavior. What if we could strategically load these heavier chunks after the initial app has fully rendered and the user has had a moment to orient themselves? Or perhaps even a little while later, giving the browser some breathing room? This approach, often called "deferred loading" or "idle-time loading," can further enhance the perceived performance and responsiveness of your application, especially when dealing with complex pages. Let's explore how we can achieve this and make those transitions even smoother. const DELAY_AFTER_RENDER_MS = 2000; const TIME_GAP_MS = 500; async function lazyRoute(importFn: () => Promise) { await importFn(); } const loadChildren = async (routes: RouteObject[]): Promise => { for (const route of routes) { if (route.lazy) { await lazyRoute(route.lazy); await wait(TIME_GAP_MS); } if (route?.children?.length) { loadChildren(route.children); } } }; export const preloadLazyComponents = () => { setTimeout(() => { loadChildren(routes); }, DEL

May 10, 2025 - 15:02
 0
Making Your React App Feel Instant: A Deep Dive into Route Optimization

Lazy loading with React Router v7 (Data Mode) = faster initial loads. Async lazy() on your route definitions handles the dynamic imports. Code for each route is chunked and fetched only when navigated to. This results in better perceived performance and less initial JS. Happy users, happy app.

import { createBrowserRouter, type RouteObject } from 'react-router-dom';

enum AppRoute { ROOT = '/', HOME = '/', ABOUT = '/about' }
export const AppRoutes = { ...AppRoute } as const;

const routes: RouteObject[] = [
  {
    path: AppRoute.ROOT,
    async lazy() { const c = await import('./components/Layout'); return { Component: c.default }; },
    children: [
      { index: true, async lazy() { const c = await import('./pages/Home'); return { Component: c.default }; } },
      { path: AppRoute.ABOUT, async lazy() { const c = await import('./pages/About'); return { Component: c.default }; } },
    ],
  },
];

const router = createBrowserRouter(routes);
export default router;

Lazy loading makes your app load fast initially, but what if the network fails when fetching a page's code? Users might just see an error. As developers, we shouldn't just accept defeat. Could we build in a way to automatically retry loading those chunks? This could make our apps more resilient, especially for users with spotty internet. Imagine a "Retry" button or an automatic background attempt to fetch the missing code. It's about making the experience smoother even when the network isn't perfect.

Here's a function called retryLazy designed to enhance the resilience of your lazy-loaded components.

export default function retryLazy<T>(
    loader: () => Promise<{ default: T }>,
    retries = 3,
    delay = 1000
): () => Promise<{ Component: T }> {
    return async () => {
        for (let i = 0; i < retries; i++) {
            try {
                const module = await loader();
                if (!module.default) {
                    throw new Error('Missing default export in module');
                }
                return { Component: module.default };
            } catch (err) {
                if (i === retries - 1) {
                    throw err;
                }
                await new Promise((res) => setTimeout(res, delay));
            }
        }
        throw new Error('Unexpected retryLazy failure');
    };
}

This retryLazy function takes a dynamic import loader and attempts to execute it up to a specified number of times with a set delay between retries. This helps gracefully handle temporary network issues when loading your lazy-loaded React components.

Now, let's see how to integrate this retryLazy function into your React Router v7 setup. Instead of directly using the lazy function provided by React, you can wrap your dynamic imports with our retryLazy function. This applies the retry mechanism to each of your lazily loaded route components.

import { createBrowserRouter, type RouteObject } from 'react-router-dom';
import retryLazy from './utils/retryLazy';

enum AppRoute {
  ROOT = '/',
  HOME = '/',
  ABOUT = '/about',
  SERVICES = '/services',
  BLOG = '/blog',
  CONTACT = '/contact'
}

export const AppRoutes = {
  ...AppRoute,
} as const;

const routes: RouteObject[] = [
  {
    path: AppRoute.ROOT,
    lazy: retryLazy(() => import('./components/Layout')),
    children: [
      {
        index: true,
        lazy: retryLazy(() => import('./pages/Home')),
      },
      {
        path: AppRoute.ABOUT,
        lazy: retryLazy(() => import('./pages/About')),
      },
      {
        path: AppRoute.SERVICES,
        lazy: retryLazy(() => import('./pages/Services')),
      },
      {
        path: AppRoute.BLOG,
        lazy: retryLazy(() => import('./pages/Blog')),
      },
      {
        path: AppRoute.CONTACT,
        lazy: retryLazy(() => import('./pages/Contact')),
      },
    ],
  },
];

const router = createBrowserRouter(routes);

export default router; 

Alright, welcome to the next level of optimizing your React Router experience! We've tackled the initial load times with lazy loading, making that first impression lightning fast. But what happens when users navigate to a page packed with code or hefty dependencies? Even with lazy loading, there can still be a noticeable delay as those larger chunks are fetched and processed.
However, we don't have to accept this as the final behavior. What if we could strategically load these heavier chunks after the initial app has fully rendered and the user has had a moment to orient themselves? Or perhaps even a little while later, giving the browser some breathing room? This approach, often called "deferred loading" or "idle-time loading," can further enhance the perceived performance and responsiveness of your application, especially when dealing with complex pages. Let's explore how we can achieve this and make those transitions even smoother.

const DELAY_AFTER_RENDER_MS = 2000;
const TIME_GAP_MS = 500;

async function lazyRoute<T>(importFn: () => Promise<T>) {
    await importFn();
}

const loadChildren = async (routes: RouteObject[]): Promise<void> => {
    for (const route of routes) {
        if (route.lazy) {
            await lazyRoute(route.lazy);
            await wait(TIME_GAP_MS);
        }
        if (route?.children?.length) {
            loadChildren(route.children);
        }
    }
};

export const preloadLazyComponents = () => {
    setTimeout(() => {
        loadChildren(routes);
    }, DELAY_AFTER_RENDER_MS);
};

To make this deferred loading strategy work, you need to trigger the preloadLazyComponents function at the right moment in your application's lifecycle. A good place to do this is within a useEffect hook in your main App component. Since useEffect with an empty dependency array runs after the initial render of your component tree, it ensures that the initial page the user sees loads quickly, and then, in the background, we start preloading the other lazy-loaded components.

import React, { useEffect } from 'react';
import { RouterProvider } from 'react-router-dom';
import router, { preloadLazyComponents } from './router';
function App() {
  useEffect(() => {
    preloadLazyComponents();
  }, []);

  return <RouterProvider router={router} />;
}

export default App;

Now, a user can land on your homepage, start browsing, and by the time they decide to navigate to another page (like "About" or "Services"), there's a good chance the code for that page has already been fetched and is ready in the background. This results in a much smoother and faster navigation experience, making your application feel more responsive and user-friendly. It's like preparing the next room before the guest even asks to go there!

Until our next coding adventure: May your bundles be small, your renders be swift, and your users be delighted! Happy coding!