Every query in the browser is a decision you can move
A component that fetches after it mounts has quietly moved work from your build machine onto a stranger's phone — and taken your read token with it.

There is a version of every component that fetches its own data. It is easy to write, it is self-contained, and it turns one prerendered page into a network waterfall that starts after the JavaScript bundle has finished parsing.
Reactive params are the trapdoor
The rule that keeps a query on the build machine is duller than it sounds: the params must be static. A reactive param makes the fetch re-run wherever the value changes, which is the browser — so paging, filtering and searching all become live requests without anyone deciding they should be.
<script setup lang="ts">
// Fetched once, at build time, into the prerendered payload.
const { data } = await useSanityContent<BlogIndexQueryResult>(
blogIndexQuery
)
// Paging and search are derived from it — no second request.
const matching = computed(() =>
(data.value?.posts ?? []).filter(matchesFilters)
)
</script>Dereference upstream, not downstream
The other half of the same rule: resolve references in the page query rather than in the child component that needs them. A navigation menu, a post's related posts and an author profile are all one arrow in a projection, and all three arrive as props.
Where the trade genuinely flips
Filtering a few hundred posts in memory is instant. Filtering fifty thousand is not, and at that point the work belongs back on a server. That is a real decision with a real threshold — which is different from arriving there by accident.

Written by
Jordan Hale
Front-end Engineer
Jordan builds front ends that stay fast after launch, and spends most of that time deleting work the browser was never asked to do.


