The simplest data path
Server Components are async functions. You await data and render. There's no useEffect, no loading state machine, no client cache to invalidate.
Two ways to read data
fetch()— for HTTP/JSON sources. Caching is controlled per call (we'll cover that in the data-fetching track).- Direct DB / SDK calls — Prisma, Drizzle, Postgres clients, internal services.
Parallel by default; waterfall on purpose
Independent fetches should be in Promise.all. The framework will not magically parallelize your sequential awaits — that's a JavaScript reality, not a framework one. Sequential is fine when the second fetch genuinely depends on the first.
Where to fetch
Fetch in the component that uses the data. Don't drill props from the page through three layers; have the leaf component fetch what it needs. The framework deduplicates identical fetch() calls during a single render pass, so two siblings asking for the same URL trigger one network request.
Map data dependencies before writing await. Start independent operations together and preserve sequence only where one result constructs the next request. Keep reads close to their consumer, but centralize authorization, caching, and error policy in shared server functions. Even when data reads live beside their components, use logs to check for duplicate calls in one request. Colocation does not remove the need to understand execution count.
Fetch deduplication does not automatically combine database SDK calls or requests with different options. Scattering an expensive query across leaves can still repeat work. Use explicit caching or a data layer when the operation, not just the URL, must be shared. Log start and end times for independent HTTP calls and dependent database calls. Move the consumers without changing policy, and count actual executions. The final tree should be easier to compose without increasing queries or weakening authorization.