Deleting the loading spinners by moving reads to the server
Four content tables, four hooks, four skeletons. Server Components turned all of it into a function call.
This portfolio used to fetch everything from the browser. Projects, posts, testimonials, a settings row — each one a hook, a query key, a loading state and a skeleton to match. Roughly this, four times over:
const { data, isLoading, error } = useQuery({
queryKey: ["portfolio"],
queryFn: () => db.from("portfolio").select("*"),
});
if (isLoading) return <Skeleton />;
It worked. It also meant every visitor got an empty page, then a spinner, then content — and crawlers got the empty page.
What it became
export default async function ProjectsPage() {
const projects = await getProjects();
return <Grid projects={projects} />;
}
No hook, no query key, no loading branch, no error branch. The data is there before the component renders, so the states that existed only to describe "not yet" stop existing.
Three things came with it:
- Content is in the HTML. View source shows the project titles. That matters for search engines and for anyone on a slow connection.
- The database client stays on the server. No connection details reach the browser, and the query is not something a visitor can reshape.
- One round trip instead of two. The old flow was: load the document, boot the JS, then start fetching.
What you give up
Being honest about the trade: this suits content that is the same for everyone and changes rarely. A dashboard full of per-user, frequently-updating data is a different problem, and client-side fetching with caching is still the right tool there.
You also lose the automatic refetch-on-focus behaviour you get free from a query library. For a portfolio, a revalidate window covers it:
export const revalidate = 60;
One caveat I hit: a loading.tsx next to a page opens a Suspense boundary across the whole segment, which starts streaming — and that has consequences for any child route that needs to return a 404. Scope it to the route that wants it.
Worth checking
If you have a page whose data is identical for every visitor, look at how many lines exist only to describe the wait. On the projects page here it was a hook, a skeleton component, an error branch and an empty state. Three of the four went away.
