Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Code Splitting
Code splitting breaks your application into smaller chunks that load on demand, improving initial load time and overall performance.
Automatic Code Splitting
React Router automatically code-splits when you use lazy():
const router = createBrowserRouter([
{
path: "/",
Component: Home,
},
{
path: "/dashboard",
lazy: () => import("./routes/dashboard"),
},
]);
The dashboard route only loads when the user navigates to /dashboard.
Framework Mode
In framework mode, routes are automatically code-split by default:
// app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes();
Each route file becomes its own chunk:
app/routes/
_index.tsx → index-[hash].js
dashboard.tsx → dashboard-[hash].js
settings.profile.tsx → settings.profile-[hash].js
Manual Code Splitting
Route Components
Split components with React.lazy() and Suspense:
import { lazy, Suspense } from "react";
const DashboardChart = lazy(() => import("./DashboardChart"));
export function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<DashboardChart />
</Suspense>
</div>
);
}
Utility Functions
Split heavy utilities:
export async function loader() {
const { processData } = await import("./heavy-utils");
const data = await fetch("/api/data").then(r => r.json());
return processData(data);
}
Third-Party Libraries
Lazy load large dependencies:
export function Component() {
const [showEditor, setShowEditor] = useState(false);
const [Editor, setEditor] = useState(null);
useEffect(() => {
if (showEditor && !Editor) {
import("monaco-editor").then(({ default: Monaco }) => {
setEditor(() => Monaco);
});
}
}, [showEditor]);
return (
<div>
<button onClick={() => setShowEditor(true)}>
Open Editor
</button>
{Editor && <Editor />}
</div>
);
}
Preloading
Improve perceived performance by preloading routes:
import { Link } from "react-router";
function Navigation() {
return (
<Link
to="/dashboard"
onMouseEnter={() => {
// Preload the route module
import("./routes/dashboard");
}}
>
Dashboard
</Link>
);
}
Preload Links Component
function PreloadLink({ to, children, ...props }) {
const preload = () => {
// Match the route and preload its module
const route = routes.find(r => r.path === to);
if (route?.lazy) {
route.lazy();
}
};
return (
<Link
to={to}
onMouseEnter={preload}
onTouchStart={preload}
{...props}
>
{children}
</Link>
);
}
Chunk Optimization
Shared Chunks
Extract common dependencies:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
charts: ['recharts', 'd3'],
},
},
},
},
});
Dynamic Import Names
Name your chunks for better debugging:
const Dashboard = lazy(
() => import(
/* webpackChunkName: "dashboard" */
"./routes/dashboard"
)
);
Bundle Analysis
Visualize Bundle Size
# Install analyzer
npm install --save-dev rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
reactRouter(),
visualizer({ open: true }),
],
});
Check Chunk Sizes
npm run build
# Output:
# dist/assets/index-a1b2c3.js 150 kB
# dist/assets/dashboard-d4e5f6.js 80 kB
# dist/assets/vendor-g7h8i9.js 200 kB
Loading States
Route Level
Show loading UI during route transitions:
import { useNavigation } from "react-router";
export function Layout() {
const navigation = useNavigation();
return (
<div>
<nav>...</nav>
{navigation.state === "loading" && (
<div className="loading-bar" />
)}
<Outlet />
</div>
);
}
Component Level
Handle component-level loading:
import { Suspense } from "react";
const Chart = lazy(() => import("./Chart"));
export function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
);
}
Error Boundaries
Handle chunk loading failures:
import { Component } from "react";
class ChunkErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
if (error.name === "ChunkLoadError") {
return { hasError: true };
}
throw error;
}
render() {
if (this.state.hasError) {
return (
<div>
<p>Failed to load. Please refresh.</p>
<button onClick={() => window.location.reload()}>
Refresh
</button>
</div>
);
}
return this.props.children;
}
}
- Split by route: Most effective code splitting strategy
- Split heavy components: Charts, editors, maps
- Don’t over-split: Too many chunks increases overhead
- Preload critical routes: On app startup or hover
- Monitor bundle size: Use visualization tools
- Cache aggressively: Set long cache headers for chunks
Framework Mode Optimizations
Route Groups
Group related routes:
// app/routes.ts
import { route, layout } from "@react-router/dev/routes";
export default [
layout("layouts/dashboard.tsx", [
route("analytics", "./dashboard/analytics.tsx"),
route("reports", "./dashboard/reports.tsx"),
]),
];
Shared Layouts
Layouts are shared across child routes (not duplicated):
routes/
_dashboard.tsx → shared layout chunk
_dashboard.analytics.tsx → analytics chunk
_dashboard.reports.tsx → reports chunk
Best Practices
- Start with routes: Automatic splitting per route
- Add component splitting: For heavy UI components
- Profile before optimizing: Use React DevTools Profiler
- Test on slow networks: Ensure good UX during loading
- Monitor real-world metrics: Track bundle sizes in CI
Common Pitfalls
❌ Splitting too aggressively
// Don't split every single component
const Button = lazy(() => import("./Button"));
✅ Split heavy features
// Split large feature components
const Dashboard = lazy(() => import("./Dashboard"));
❌ Forgetting Suspense
const Chart = lazy(() => import("./Chart"));
<Chart /> // Error: Missing Suspense boundary
✅ Wrap with Suspense
<Suspense fallback={<Spinner />}>
<Chart />
</Suspense>