4 Practical Ways to Speed Up Your Loaders in React Router v7

Alem Tuzlak
AuthorAlem Tuzlak

React Router v7 gives you incredible flexibility in how you fetch and deliver data through loaders. But flexibility can mean you can accidentally slow things down. Here are four practical ways to make your loaders noticeably faster — with real before/after examples.

1. Batch Independent Work with Promise.all

If your loader calls multiple independent tasks, run them in parallel instead of waiting for each one to finish before starting the next.

❌ Before — Waterfall


export async function loader({ params }: Route.LoaderArgs) {
const product = await fetchProduct(params.productId!); // 400ms
const reviews = await fetchProductReviews(params.productId!); // 300ms
const variations = await fetchProductVariations(params.productId!); // 200ms
return { product, reviews, variations }; // total: ~900ms
}

Each request waits for the previous one, adding up all their durations.

✅ After — Parallelized with Promise.all


export async function loader({ params }: Route.LoaderArgs) {
const id = params.productId!;
const [product, reviews, variations] = await Promise.all([
fetchProduct(id), // 400ms
fetchProductReviews(id), // 300ms
fetchProductVariations(id), // 200ms
]);
return { product, reviews, variations }; // total: ~400ms
}

Now the loader resolves in roughly the time of the slowest call — not the sum of all three.

2. Reduce Database Round-Trips

If you control your ORM or SQL, avoid multiple small queries and fetch all related data in one shot. This will speed up your loaders by reducing network latency and leveraging database joins to get everything in one go.

❌ Before — Two Separate Queries


export async function loader({ params }: Route.LoaderArgs) {
const id = params.productId!;
const product = await db.product.findUnique({ where: { id } }); // 200ms
const [reviews, variations] = await Promise.all([
db.review.findMany({ where: { productId: product.id } }), // 150ms
db.variation.findMany({ where: { productId: product.id } }), // 100ms
]);
return { product, reviews, variations }; // total: ~350ms
}

Two separate DB round-trips even if parallelized are still suboptimal.

✅ After — One Query with Relations


export async function loader({ params }: Route.LoaderArgs) {
const id = params.productId!;
const product = await db.product.findUnique({
where: { id },
include: {
reviews: { take: 20, orderBy: { createdAt: "desc" } },
variations: true,
},
}); // 200ms
return { product }; // total: ~200ms
}

This reduces network hops and leverages database joins or relation loading internally. It’s often the single biggest win when optimizing Remix/React Router v7 loaders, especially on high-latency DB connections or far-away servers.

3. Stream Slow Data with the New Loader Props API

Loaders can return promises directly, and those promises are streamed to the component as they resolve — no special APIs required. So you can leverage React’s built-in Suspense to show loading states for slow data without blocking the whole page.

❌ Before — Blocking Everything


export async function loader({ params }: Route.LoaderArgs) {
const id = params.productId!;
const product = await getProductBundle(id); // 500ms
const alsoBought = await getAlsoBoughtRecommendations(id); // 3000ms
return { product, alsoBought }; // total: ~3500ms
}
// resolved in ~3500ms
export function ProductRoute({ loaderData }: Route.ComponentProps) {
return (
<>
<ProductHero data={loaderData.product} />
<Recommendations items={loaderData.alsoBought} />
</>
);
}

Here the getAlsoBoughtRecommendations call blocks the whole loader, delaying HTML streaming and TTFB. It takes it 3 seconds to resolve after the initial product data is ready. This would bring your app to a standstill on slow connections.

✅ After — Stream Promises

In v7 framework mode, just return the promise and React Router handles streaming + suspense automatically:


export async function loader({ params }: Route.LoaderArgs) {
const id = params.productId!;
const product = await getProductBundle(id); // 500ms
return {
product,
alsoBought: getAlsoBoughtRecommendations(id), // returns a Promise (no await)
};
}
// resolved in ~500ms
export function ProductRoute({ loaderData }: Route.ComponentProps) {
return (
<>
<ProductHero data={loaderData.product} />
<React.Suspense fallback={<SkeletonRecommendations />}>
<Recommendations items={loaderData.alsoBought} />
</React.Suspense>
</>
);
}

This pattern keeps your loader fast and your slow secondary requests off the critical path.

4. Location Still Matters — Latency Kills

Even with perfect code, distance between your server and your database adds unavoidable round-trip time. If your app runs in Frankfurt but your DB is in Virginia, every loader call adds ~100 ms latency minimum. When possible:

  • Host your DB and your server in the same region.
  • Use read replicas close to your server for heavy read routes.
  • Check your ORM logs or APM traces — if DB latency is consistently > 30 ms, co-location can save more time than any code tweak.

Wrap-Up

React Router v7 framework mode gives you the best of both worlds — flexible async loaders and automatic streaming. But performance still depends on how you structure your work:

  • Batch independent work (Promise.all)
  • Reduce database round-trips
  • Stream slow data using promise-aware loaderData
  • Co-locate your DB with your server Small fixes here often shave hundreds of milliseconds off TTFB and let React Router’s new data-driven rendering shine.

Share this article

alem tuzlak profile picture
Written by Alem Tuzlak

Alem Tuzlak is an experienced software developer, co-founder of Forge 42, and a recognized expert in Remix.run and React Router. With a strong focus on open-source development, Alem has created and maintained several high-impact projects such as Remix Development Tools, Remix-Toast, Remix-Client-Cache, and Remix-Hook-Form. These open-source projects have accumulated over a few million downloads, with weekly counts going over 150k. Alem is passionate about modern web development, specifically server-side rendering (SSR), event-driven architectures, and optimizing developer experience (DX). Alem’s work extends beyond coding, as he also contributes to fostering regional engagement with developer communities. He organizes events like Supabase and Remix-run meetups in Sarajevo, where he brings together developers to learn and collaborate. As an entrepreneur, Alem is dedicated to scaling his agency, Forge 42, which specializes in Remix-run consulting and open-source solutions. His leadership focuses on creating impactful tools while also building a strong and sustainable business.

Join 40,000+ developers in the Epic Web community

Get the latest tutorials, articles, and announcements delivered to your inbox.

I respect your privacy. Unsubscribe at any time.