How to Optimize React Router Performance

Optimizing React Router is crucial for improving the loading speed and overall performance of your React applications. This article explores essential techniques to streamline your routing, including route-based code splitting with React.lazy, leveraging React Router v6 data APIs to prevent data-fetching waterfalls, prefetching assets on user intent, and minimizing unnecessary re-renders in layout components.

Implement Route-Based Code Splitting

By default, bundling tools pack all your routes into a single JavaScript file, leading to slow initial page loads. Route-based code splitting ensures that users only download the code required for the page they are currently viewing.

You can achieve this by combining React.lazy() with React’s Suspense component:

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

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

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

Utilize React Router v6 Data Loaders

React Router v6.4+ introduced data APIs like loader and action. Traditionally, components render, mount, and then fetch data (creating a “render-then-fetch” waterfall).

Using the loader function allows React Router to fetch data for a route in parallel with fetching and rendering the route component itself. This dramatically reduces perceived loading times.

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Dashboard, { dashboardLoader } from './pages/Dashboard';

const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: <Dashboard />,
    loader: dashboardLoader, // Fetches data before rendering
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

Prefetch Routes on User Intent

To make transitions feel instantaneous, you can prefetch the code and data for a route before the user actually clicks a link. A common pattern is to trigger a prefetch when a user hovers over a navigation link.

For lazy-loaded components, you can trigger the dynamic import on hover:

const prefetchDashboard = () => {
  import('./pages/Dashboard');
};

// In your navigation component
<Link to="/dashboard" onMouseEnter={prefetchDashboard}>
  Dashboard
</Link>

If you are using React Router’s data loaders, you can also query and cache the loader data on hover.

Optimize Layout Re-renders with <Outlet>

In React Router, nested routes are rendered inside a parent layout using the <Outlet /> component. To prevent the entire layout from re-rendering when a nested route changes, ensure your layout components are lightweight and do not hold unnecessary state that changes on route transitions.

Keep state local to the leaf routes rather than lifting it to the main layout route unless absolutely required. If the layout must contain expensive sub-components, wrap them in React.memo to isolate them from routing changes.