Designing Performant Data Fetching in Modern Web Applications

Ab Devweb
Author
Data fetching is one of those topics every developer thinks they understand… until performance issues appear in production. Slow pages, waterfalls of requests, duplicated queries, and inconsistent loading states are rarely caused by the database alone. Most of the time, the real problem is how data is fetched, cached, and coordinated across the application.
Modern frameworks give us powerful tools, but using them well requires a bit of discipline and a clear mental model.
The Core Principles of Good Data Fetching
Before touching code, a few principles should guide every decision:
- Fetch data as close as possible to where it is needed
- Avoid fetching the same data multiple times
- Cache aggressively, invalidate intentionally
- Separate read patterns from write patterns
Ignoring these rules often leads to fragile and slow applications, no matter how modern the stack looks on paper.
Fetching on the Server First
Server-side data fetching reduces the amount of work the browser has to do and improves the initial load. Instead of sending an empty shell and asking the client to do everything, the server can deliver meaningful HTML immediately.
Here is a simple example using a server-side function to fetch data before rendering:
// server/data.ts
export async function getUsers() {
const res = await fetch("https://api.example.com/users", {
cache: "no-store",
})
if (!res.ok) {
throw new Error("Failed to fetch users")
}
return res.json()
}
By centralizing data access, you avoid duplicating fetch logic across components and reduce the risk of inconsistent behavior.
Controlling Caching Explicitly
Caching is not an optimization detail, it is a design choice. Uncontrolled caching leads to stale data, while no caching at all leads to unnecessary load and poor performance.
A simple example with explicit caching behavior:
export async function getProducts() {
return fetch("https://api.example.com/products", {
next: {
revalidate: 60, // seconds
},
}).then(res => res.json())
}
This approach makes freshness a conscious decision instead of an accidental side effect.
Avoiding Request Waterfalls
One of the most common performance traps is the request waterfall: one request depends on the result of another, which depends on another, and so on.
Bad pattern:
Comments (6)