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, open-source maintainer, and AI tooling engineer focused on building practical, developer-first software. He currently works at CopilotKit and is the lead maintainer of TanStack AI, where he helps shape modern patterns for building AI-powered applications with TypeScript, React, and full-stack web technologies. Previously, Alem built and maintained several widely used open-source projects in the Remix.run and React Router ecosystem, including Remix Development Tools, Remix-Toast, Remix-Client-Cache, and Remix-Hook-Form, reaching millions of downloads and strong adoption across the community. His work sits at the intersection of AI, developer experience, server-side rendering, event-driven architectures, and modern web application design. Beyond building tools, Alem is passionate about teaching developers how to use emerging technologies in practical, production-ready ways. Through his courses, Alem helps developers understand how to build real AI applications, integrate AI into existing products, and reason about the architecture, workflows, and developer experience behind modern AI-powered software.

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.