MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Router v6: A Complete Guide

Master React Router v6: nested routes, Outlet, useNavigate, loaders, and data APIs.

React RouterReactRoutingFrontend

By MinhVo

Introduction

React Router v6 represents a paradigm shift in client-side routing for React applications. As the most widely adopted routing library in the React ecosystem, React Router has evolved significantly from its early days of simple route matching to a sophisticated, component-based routing system that aligns naturally with how React developers think about UI composition. The v6 release introduces fundamental changes that make routing more intuitive, performant, and deeply integrated with React's component model.

Understanding React Router v6 is essential for any frontend developer building single-page applications in 2024 and beyond. The library introduces a declarative routing approach where routes are defined as components, enabling powerful patterns like nested layouts, data loading via loaders, and seamless programmatic navigation. Whether you're migrating from v5 or starting a new project, mastering these concepts will significantly improve your development workflow and application architecture.

The shift from v5 to v6 isn't just an incremental update—it's a complete rethinking of how routing should work in React. The removal of <Switch> in favor of <Routes>, the introduction of the <Outlet> component for nested rendering, and the new useNavigate hook replacing useHistory all reflect a philosophy of making routing simpler, more predictable, and more composable. In this comprehensive guide, we'll explore every aspect of React Router v6 from fundamental concepts to advanced production patterns.

React Router v6 Architecture

Understanding React Router v6: Core Concepts

The Component-Based Routing Model

React Router v6 treats routes as first-class React components. The <Routes> component replaces the v5 <Switch> component and provides automatic route ranking, relative routing, and nested route support. This approach eliminates the need for explicit exact prop handling and simplifies route configuration dramatically.

The core philosophy of v6 is that routing should be a natural extension of React's component tree. Routes are defined declaratively using <Route> components, which can be nested to create hierarchical layouts. This mirrors how UI is naturally structured—a dashboard has a sidebar, a header, and a content area; the content area changes based on the URL while the sidebar and header remain persistent.

Key API Changes from v5

React Router v6 introduces several breaking changes that modernize the routing experience. The <Switch> component is replaced by <Routes>, useHistory is replaced by useNavigate, and the component prop is replaced by element which accepts JSX directly. The <Redirect> component is replaced by <Navigate>.

One of the most significant changes is the introduction of relative routing. Links and navigations can now use relative paths without specifying the full URL. In v5, if you were at /dashboard/settings and wanted to link to /dashboard/profile, you needed the full path. In v6, you can simply use to="profile" from any route nested under /dashboard.

The useParams, useSearchParams, and useLocation hooks remain but are joined by useNavigate which returns a function rather than a history object. The navigation function accepts a path string or a number (for going back/forward), and an options object for replace, state, and preventScrollReset.

Route Matching Algorithm

V6 uses a sophisticated ranking algorithm to determine which route matches a given URL. Unlike v5's first-match approach, v6 scores all matching routes and selects the most specific one. This eliminates the need for exact props and ensures that routes are matched in a predictable, deterministic manner regardless of the order they're defined.

The ranking system considers several factors: static segments score higher than dynamic parameters, which score higher than catch-all splat routes. This means <Route path="users/:id"> will always match before <Route path="users/*"> for a URL like /users/123, even if the splat route is defined first.

Nested Route Architecture

React Router v6's nested routing model allows developers to build complex layouts with minimal boilerplate. Parent routes define the layout structure, while child routes render within the parent's <Outlet> component. This pattern naturally maps to common UI patterns like dashboards, settings pages, and multi-step wizards.

The nested architecture promotes code reuse by allowing shared layouts to be defined once at the parent route level. Navigation elements like sidebars, headers, and breadcrumbs can be implemented in the parent component and automatically applied to all child routes without any prop drilling or context management.

Data Loading with Loaders

While data fetching was primarily handled in useEffect in v5 (and often in a separate library like react-query), v6 introduces a first-class loader function on route objects. Loaders execute before the route renders, ensuring that data is available when the component mounts. This eliminates loading spinners for initial data and provides automatic error handling.

The loader function receives a params object containing route parameters and a request object containing the URL. It can return any serializable data, which is then accessible in the component via the useLoaderData hook. If the loader throws or returns a rejected Response, React Router will render the nearest errorElement.

Nested Routing Architecture

Architecture and Design Patterns

Outlet Component Pattern

The <Outlet> component is the cornerstone of nested routing in v6. It acts as a placeholder in parent components where child routes render. This pattern enables developers to create persistent layouts that don't re-render when navigating between child routes, improving performance and maintaining UI state like scroll position and form input.

The Outlet pattern supports deeply nested layouts. For example, a dashboard might have a main layout with a sidebar, and within that, a settings section with its own sub-navigation. Each level defines its own layout and content area using nested <Outlet> components.

Route Configuration Objects

For large applications, routes can be defined as configuration objects and passed to createBrowserRouter. This approach enables dynamic route generation based on user permissions, feature flags, or application configuration. It also centralizes route definitions, making them easier to audit and test.

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { index: true, element: <Home /> },
      {
        path: "dashboard",
        element: <DashboardLayout />,
        loader: dashboardLoader,
        children: [
          { index: true, element: <DashboardHome /> },
          { path: "analytics", element: <Analytics /> },
          { path: "settings", element: <Settings /> },
        ],
      },
    ],
  },
]);

Data Router vs. Component Router

React Router v6.4 introduced the data router pattern via createBrowserRouter, which adds loader/action support to the traditional component-based router. The data router is the recommended approach for new applications as it provides better data loading, mutation, and error handling patterns.

The component router (<BrowserRouter>) is still supported and works well for simple applications that don't need server-side data loading. The choice between them depends on your application's data requirements and whether you're using a framework like Remix/React Router v7.

Step-by-Step Implementation

Installation and Basic Setup

Setting up React Router v6 requires installing the package and configuring the router provider at the application root:

npm install react-router-dom@6
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Home } from './pages/Home';
import { About } from './pages/About';
import { NotFound } from './pages/NotFound';
 
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

Implementing Nested Routes with Outlet

Nested routes are the most powerful feature of React Router v6. The parent route defines the layout structure using the <Outlet> component, while child routes provide the content:

// Layout component
import { Outlet, Link, NavLink } from 'react-router-dom';
 
function DashboardLayout() {
  return (
    <div className="flex h-screen">
      <aside className="w-64 bg-gray-900 text-white p-4">
        <nav className="space-y-2">
          <NavLink to="profile" className={({isActive}) => 
            isActive ? 'text-blue-400' : 'text-gray-300'
          }>
            Profile
          </NavLink>
          <NavLink to="settings" className={({isActive}) => 
            isActive ? 'text-blue-400' : 'text-gray-300'
          }>
            Settings
          </NavLink>
          <NavLink to="analytics" className={({isActive}) => 
            isActive ? 'text-blue-400' : 'text-gray-300'
          }>
            Analytics
          </NavLink>
        </nav>
      </aside>
      <main className="flex-1 overflow-auto p-6">
        <Outlet />
      </main>
    </div>
  );
}
 
// Route configuration
<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} />
  <Route path="profile" element={<Profile />} />
  <Route path="settings" element={<Settings />} />
  <Route path="analytics" element={<Analytics />} />
</Route>

Programmatic Navigation with useNavigate

The useNavigate hook provides imperative navigation capabilities for scenarios where declarative <Link> components aren't sufficient:

import { useNavigate } from 'react-router-dom';
 
function LoginForm() {
  const navigate = useNavigate();
  const [error, setError] = useState(null);
  
  const handleSubmit = async (formData) => {
    try {
      const user = await authService.login(formData);
      // Navigate to dashboard, replacing the login page in history
      navigate('/dashboard', { 
        replace: true,
        state: { welcomeBack: true }
      });
    } catch (err) {
      setError(err.message);
      // Stay on login page with error state
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      {/* form fields */}
    </form>
  );
}

Data Loading with Loaders

The loader pattern ensures data is available before the component renders, providing instant page loads without loading spinners:

import { 
  createBrowserRouter, 
  RouterProvider, 
  useLoaderData,
  useRouteError 
} from 'react-router-dom';
 
// Loader function - runs before component renders
async function userLoader({ params }) {
  const response = await fetch(`/api/users/${params.userId}`);
  if (!response.ok) {
    throw new Response('User not found', { 
      status: 404,
      statusText: 'User Not Found' 
    });
  }
  return response.json();
}
 
// Component that uses loader data
function UserProfile() {
  const user = useLoaderData();
  
  return (
    <div className="profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.bio}</p>
    </div>
  );
}
 
// Error boundary for this route
function UserError() {
  const error = useRouteError();
  return (
    <div className="error-page">
      <h1>{error.status}</h1>
      <p>{error.statusText}</p>
    </div>
  );
}
 
// Route definition
const router = createBrowserRouter([
  {
    path: '/users/:userId',
    element: <UserProfile />,
    loader: userLoader,
    errorElement: <UserError />,
  },
]);

Form Actions and Mutations

React Router v6.4+ introduced the action pattern for handling form submissions and data mutations:

async function createUserAction({ request }) {
  const formData = await request.formData();
  const name = formData.get('name');
  const email = formData.get('email');
  
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, email }),
  });
  
  if (!response.ok) {
    return { error: 'Failed to create user' };
  }
  
  const user = await response.json();
  return redirect(`/users/${user.id}`);
}
 
function CreateUser() {
  const actionData = useActionData();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === 'submitting';
  
  return (
    <Form method="post">
      {actionData?.error && <p className="error">{actionData.error}</p>}
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Creating...' : 'Create User'}
      </button>
    </Form>
  );
}

Data Loading and Actions

Real-World Use Cases and Case Studies

Use Case 1: Multi-Step Form Wizard

React Router v6's nested routing is perfect for building multi-step forms where each step is a child route with its own component, while the parent route handles form state persistence. The <Outlet> component ensures that form data persists between steps without complex state management.

function CheckoutLayout() {
  const [formData, setFormData] = useState({});
  
  return (
    <div className="checkout">
      <StepIndicator />
      <Outlet context={{ formData, setFormData }} />
    </div>
  );
}
 
// Child step component
function ShippingStep() {
  const { formData, setFormData } = useOutletContext();
  const navigate = useNavigate();
  
  const handleNext = (shippingData) => {
    setFormData(prev => ({ ...prev, shipping: shippingData }));
    navigate('../payment'); // Relative navigation
  };
  
  return <ShippingForm onSubmit={handleNext} defaultValues={formData.shipping} />;
}

Use Case 2: Dashboard with Role-Based Navigation

Dashboard applications with role-based navigation leverage React Router's flexible route configuration. Different user roles see different navigation items and have access to different routes, all managed through route guards and dynamic route generation.

const getRoutes = (userRole) => {
  const routes = [
    { path: 'overview', element: <Overview /> },
    { path: 'profile', element: <Profile /> },
  ];
  
  if (userRole === 'admin') {
    routes.push(
      { path: 'users', element: <UserManagement /> },
      { path: 'settings', element: <SystemSettings /> },
      { path: 'logs', element: <AuditLogs /> }
    );
  }
  
  if (userRole === 'editor' || userRole === 'admin') {
    routes.push(
      { path: 'content', element: <ContentManager /> }
    );
  }
  
  return routes;
};

Use Case 3: E-Commerce Product Catalog

Product catalogs with category navigation, filtering, and product detail pages leverage React Router v6's nested routing effectively. Category pages serve as parent routes with product listings as child routes, while product detail pages are nested deeper with their own loaders for fetching product data.

const router = createBrowserRouter([
  {
    path: '/shop',
    element: <ShopLayout />,
    children: [
      {
        index: true,
        element: <FeaturedProducts />,
        loader: featuredProductsLoader,
      },
      {
        path: ':category',
        element: <CategoryPage />,
        loader: categoryLoader,
        children: [
          {
            path: ':productId',
            element: <ProductDetail />,
            loader: productLoader,
          },
        ],
      },
    ],
  },
]);

Best Practices for Production

  1. Use Nested Routes for Layouts: Nested routes eliminate layout wrapper components and ensure layouts persist during navigation. This improves performance by preventing unnecessary re-renders of layout components when navigating between child routes.

  2. Implement Proper Error Boundaries: React Router v6 provides built-in error boundary support through errorElement. Create meaningful error pages that maintain the application layout and provide recovery options. Always include error boundaries at the root route to catch unexpected errors.

  3. Leverage Relative Routing: Use relative paths in Links and useNavigate calls to create components that work correctly regardless of where they're mounted in the route tree. This makes components more reusable and reduces coupling between route configuration and component code.

  4. Use Loaders for Data Fetching: The loader pattern provides a better user experience by ensuring data is available before components render. This eliminates loading spinners and prevents layout shifts. Loaders also enable automatic error handling and support deferred loading for non-critical data.

  5. Implement Route Guards at the Route Level: Route guards should be implemented using loader functions or wrapper components at the route level, not inside individual components. This ensures protected routes are secured regardless of how they're accessed.

  6. Use NavLink for Active States: The NavLink component provides built-in active state detection through the isActive property in className/style functions. This eliminates manual active state tracking and ensures navigation highlights are always correct.

  7. Optimize Bundle Splitting with React.lazy: Use React.lazy and <Suspense> with route-based code splitting to reduce initial bundle size. Each route can be dynamically imported, ensuring users only download code needed for the current page.

  8. Persist Scroll Position: Use the useScrollRestoration hook or the scrollRestoration option on the router to maintain scroll position during back/forward navigation. For new navigations, scroll to the top to ensure users see the page content from the start.

Common Pitfalls and Solutions

PitfallImpactSolution
Forgetting to migrate from Switch to RoutesRoutes don't render, blank pageReplace all <Switch> with <Routes> and component with element
Missing Outlet in parent layoutChild routes render nothingAlways add <Outlet /> where child routes should render
Using absolute paths in nested routesNavigation breaks when route hierarchy changesUse relative paths without leading slash in nested contexts
Not handling loader errorsApplication crashes on failed fetchAlways add errorElement to routes with loaders
State loss during navigationForm data disappears between pagesUse useOutletContext or URL search params for persistence
Using useNavigate in renderInfinite re-render loopsCall navigate in event handlers or effects, not during render
Incorrect route nesting orderLayouts apply to wrong routesVerify route hierarchy matches the intended component tree
Not using replace for redirectsBroken browser historyUse { replace: true } when redirecting users

Performance Optimization

React Router v6 includes several performance optimizations. The route matching algorithm uses a trie-based approach for fast lookups, and the component model prevents unnecessary re-renders when routes haven't changed.

Code Splitting with React.lazy

import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';
 
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
 
const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: (
      <Suspense fallback={<DashboardSkeleton />}>
        <Dashboard />
      </Suspense>
    ),
    children: [
      {
        path: 'settings',
        element: (
          <Suspense fallback={<SettingsSkeleton />}>
            <Settings />
          </Suspense>
        ),
      },
      {
        path: 'analytics',
        element: (
          <Suspense fallback={<AnalyticsSkeleton />}>
            <Analytics />
          </Suspense>
        ),
      },
    ],
  },
]);

Deferred Data Loading

For non-critical data that shouldn't block rendering, use defer to load data in parallel:

import { defer, useLoaderData, Await } from 'react-router-dom';
 
async function dashboardLoader() {
  // Critical data - blocks rendering
  const userPromise = fetch('/api/user').then(r => r.json());
  
  // Non-critical data - loads in parallel
  const analyticsPromise = fetch('/api/analytics').then(r => r.json());
  const notificationsPromise = fetch('/api/notifications').then(r => r.json());
  
  return defer({
    user: await userPromise, // Awaited - blocks
    analytics: analyticsPromise, // Deferred - parallel
    notifications: notificationsPromise, // Deferred - parallel
  });
}
 
function Dashboard() {
  const { user, analytics, notifications } = useLoaderData();
  
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Await resolve={analytics}>
          {(data) => <AnalyticsChart data={data} />}
        </Await>
      </Suspense>
      <Suspense fallback={<NotificationsSkeleton />}>
        <Await resolve={notifications}>
          {(data) => <NotificationList items={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

Memoizing Route Components

For routes with expensive computations, wrap components in React.memo and use useMemo for derived data to prevent unnecessary re-renders when the route hasn't changed but parent state has.

Comparison with Alternatives

FeatureReact Router v6TanStack RouterNext.js App Router
Learning CurveLowMedium-HighMedium
Type SafetyLimited (manual)Full TypeScript inferenceFull TypeScript
Data LoadingLoaders/ActionsLoaders with fine-grained invalidationServer Components
Bundle Size~13KB gzipped~8KB gzippedBuilt into framework
SSR SupportManual setupBuilt-in with adaptersFirst-class
File-based RoutingPlugin availableBuilt-inBuilt-in
Route GuardsManual implementationBuilt-in beforeLoadMiddleware
Search Param ValidationManual with useSearchParamsFull Zod integrationBuilt-in
Ideal UseSPAs, existing React appsType-heavy applicationsFull-stack React apps

Advanced Patterns and Techniques

Custom Route Hooks

Create custom hooks that wrap React Router's hooks to provide application-specific navigation logic and type safety:

import { useNavigate, useLocation, useParams } from 'react-router-dom';
import { useCallback, useMemo } from 'react';
 
function useAppNavigation() {
  const navigate = useNavigate();
  const location = useLocation();
  
  const goToDashboard = useCallback(() => {
    navigate('/dashboard', { 
      state: { from: location.pathname } 
    });
  }, [navigate, location]);
  
  const goToProfile = useCallback((userId) => {
    navigate(`/users/${userId}`);
  }, [navigate]);
  
  const goBack = useCallback(() => {
    navigate(-1);
  }, [navigate]);
  
  const breadcrumbs = useMemo(() => {
    const segments = location.pathname.split('/').filter(Boolean);
    return segments.map((segment, index) => ({
      label: segment.charAt(0).toUpperCase() + segment.slice(1),
      path: '/' + segments.slice(0, index + 1).join('/'),
    }));
  }, [location.pathname]);
  
  return { goToDashboard, goToProfile, goBack, breadcrumbs, currentPath: location.pathname };
}

Animated Route Transitions

Implement smooth route transitions using CSS transitions and the useLocation hook:

import { useLocation, useNavigationType } from 'react-router-dom';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
 
function AnimatedRoutes() {
  const location = useLocation();
  const navigationType = useNavigationType();
  
  return (
    <TransitionGroup 
      className="route-container"
      childFactory={(child) => 
        React.cloneElement(child, {
          classNames: navigationType === 'PUSH' ? 'slide-left' : 'slide-right',
        })
      }
    >
      <CSSTransition 
        key={location.pathname} 
        timeout={300} 
        classNames="slide-left"
      >
        <Routes location={location}>
          {/* Route definitions */}
        </Routes>
      </CSSTransition>
    </TransitionGroup>
  );
}

Route-Level Authentication Guard

function ProtectedRoute({ children, requiredRole }) {
  const { user, isLoading } = useAuth();
  const location = useLocation();
  
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return children;
}
 
// Usage in route configuration
{
  path: '/admin',
  element: (
    <ProtectedRoute requiredRole="admin">
      <AdminLayout />
    </ProtectedRoute>
  ),
}

Testing Strategies

Unit Testing with MemoryRouter

import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
 
test('renders user profile based on route params', () => {
  render(
    <MemoryRouter initialEntries={['/users/123']}>
      <Routes>
        <Route path="/users/:userId" element={<UserProfile />} />
      </Routes>
    </MemoryRouter>
  );
  
  expect(screen.getByText('User Profile')).toBeInTheDocument();
});
 
test('navigates to settings when link is clicked', async () => {
  const user = userEvent.setup();
  
  render(
    <MemoryRouter initialEntries={['/dashboard']}>
      <Routes>
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Routes>
    </MemoryRouter>
  );
  
  await user.click(screen.getByText('Settings'));
  expect(screen.getByText('Settings Page')).toBeInTheDocument();
});

Integration Testing with Data Loaders

test('loads and displays user data from loader', async () => {
  const mockUser = { id: '123', name: 'John Doe', email: 'john@example.com' };
  
  const router = createMemoryRouter([
    {
      path: '/users/:userId',
      element: <UserProfile />,
      loader: () => mockUser,
    },
  ], { initialEntries: ['/users/123'] });
  
  render(<RouterProvider router={router} />);
  
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });
});

Future Outlook

React Router continues to evolve with the React ecosystem. The library is moving towards tighter integration with React Server Components and streaming data patterns. React Router v7 (already available in framework mode) represents the next step, merging React Router with Remix's data loading patterns and adding file-based routing as a first-class feature.

The React Router team is also working on improving the developer experience with better TypeScript integration, improved error messages, and enhanced debugging tools. Features like automatic route type generation and loader/action type inference will make React Router even more productive for TypeScript developers.

The convergence of React Router and Remix means that skills learned with React Router v6 will transfer directly to the full-stack framework. Loaders, actions, and the data router pattern are all foundational concepts that will remain relevant as the ecosystem evolves.

Conclusion

React Router v6 represents a significant advancement in client-side routing for React applications. The library's component-based approach, nested routing model, and data loading patterns provide a solid foundation for building complex, maintainable applications at any scale.

Key takeaways for mastering React Router v6:

  1. Embrace the component-based routing model — routes are components, and composition is your primary tool
  2. Use nested routes and Outlet — they eliminate layout boilerplate and ensure consistent UI across navigations
  3. Implement loaders for data fetching — this eliminates loading states and provides automatic error handling
  4. Leverage relative routing — it makes components reusable regardless of their position in the route tree
  5. Use the data router pattern — createBrowserRouter with loaders and actions is the recommended approach for production applications
  6. Implement proper error boundaries — always add errorElement to routes that fetch data
  7. Optimize with code splitting — use React.lazy and route-based splitting to reduce initial bundle size

By following the patterns and best practices outlined in this guide, you can leverage React Router v6 to build sophisticated routing solutions that scale with your application's complexity while maintaining excellent performance and developer experience.