SaaS500+
daily active users
SaaS Analytics
A reporting dashboard that replaced a fortnightly spreadsheet ritual nobody enjoyed and everybody depended on.
- Custom dashboard
- Data viz
- Next.js
Server components, caching, data fetching, and the four mistakes we've made on real App Router projects so you don't have to repeat them.
Bilal Rehman
Lead Developer, Quesiono
We've shipped a dozen App Router projects now, from marketing sites to a reporting dashboard serving 500 daily users. This is what we've settled on, including the parts we got wrong first.
The default should be a server component. Add "use client" when you need state, an effect, a browser API, or an event handler — not before.
The mistake we made early was putting "use client" at the top of a page because one child needed interactivity. That pulls the whole subtree into the client bundle. The fix is to push the boundary as far down as it goes: keep the page and its layout on the server, and make the interactive leaf its own client component.
A concrete example. A listing page with 40 property cards and a filter bar. Wrong version: the page is a client component, so all 40 cards ship as client JavaScript. Right version: the page and cards are server components, and the filter bar is one client component that receives the dataset as a prop. Same behaviour, a fraction of the bundle.
App Router caching confuses people because there are four distinct layers and the error messages don't tell you which one you're hitting:
next: { revalidate } or cache.The practical rule: be explicit. Set revalidation on every fetch rather than relying on defaults, because the defaults have changed between versions and will change again. When something serves stale data, work out which layer is holding it before reaching for revalidatePath everywhere.
For content-driven pages we use time-based revalidation with a webhook-triggered revalidateTag from the CMS. Editors publish, the tag invalidates, the next request rebuilds. No full deploy.
Coming from the Pages Router the instinct is to gather all data at the page level and pass it down. In App Router that's usually worse.
Fetch inside the component that needs the data. Request memoization prevents duplicate work, components become self-contained, and you can wrap each one in its own Suspense boundary so a slow section doesn't hold up the fast ones.
The counter-case: when two sibling components need related data and you want a single query rather than two, hoist it. Don't be dogmatic about it.
Do watch for the sequential waterfall. Awaiting one fetch and then another in the same component serialises them. If they're independent, Promise.all them.
Streaming is the App Router's best feature and it's easy to under-use. Wrap slow sections in Suspense with a skeleton that matches the final layout's dimensions — matching the dimensions matters, because a skeleton that's the wrong height causes the layout shift you were trying to avoid.
On the dashboard we built, the shell and navigation render immediately while fifteen chart panels stream in. Time to first paint is under a second even when the slowest query takes three.
Don't wrap everything. A Suspense boundary around something that resolves in 20ms adds a flash of skeleton for no reason.
Server actions are excellent for mutations tied to a form. We started using them for data fetching too, because it felt tidy to have one mechanism.
They're not designed for that. Server actions run sequentially, they don't benefit from the data cache, and debugging a failed action is worse than debugging a route handler. We now use them for mutations and Route Handlers or direct server-component fetches for reads.
Reading cookies(), headers(), or searchParams opts the entire route into dynamic rendering. We had a marketing page drop out of static generation because a shared analytics helper read a header, and nobody noticed until build output showed it as a lambda.
Check the build output. Next prints a symbol next to each route showing static, dynamic, or ISR. If a page you expected to be static is dynamic, something is reading a request-time API — trace it before shipping.
Parallel and intercepting routes are genuinely clever and we reached for them to build a photo lightbox with a shareable URL. It worked. It also took two days, produced a route structure nobody else on the team could follow, and broke in a way that took another half-day to diagnose after a minor version bump.
We replaced it with a query parameter and a client component in about an hour. The lesson isn't that the feature is bad — it's that a clever routing solution needs to earn its complexity against something simpler.
Our first App Router project fetched from a CMS and typed responses as any at the boundary, planning to tighten it later. Later arrived as a runtime error in production when a field was renamed.
Now every external response goes through a Zod schema at the boundary. It costs an hour per integration and catches the class of bug where the API changed and TypeScript happily compiled anyway.
The Image component handles format negotiation, sizing, and lazy loading, but only if you feed it correctly. Always pass width and height (or fill with a sized parent), always set sizes when using fill, and set priority on the LCP image.
On next.config: if you don't serve remote images, don't configure remote patterns. A wildcard hostname combined with dangerouslyAllowSVG lets any origin push a script-bearing SVG through your optimiser. We found exactly that configuration on a site we inherited — including our own, in an early version of this one.
For fonts, next/font self-hosts and generates a metric-matched fallback, which removes both the third-party request and most of the font-swap layout shift. Load the weights you use and no more.
Two minutes after every build:
Most of the performance problems we've caught before launch came from reading that output rather than from profiling.
If you're planning a Next.js build and want a sanity check on the architecture before committing, our web development engagements start with exactly that conversation.
If you'd rather not run this yourself, these are the pages that cover it.
SaaS500+
daily active users
A reporting dashboard that replaced a fortnightly spreadsheet ritual nobody enjoyed and everybody depended on.
2.4×
organic enquiries
480 listings, eight photos each, and a search that has to feel instant on a phone in a car park.
LCP, CLS, and INP explained by cause rather than definition — and the specific fixes that moved each one on real sites, in the order we try them.
Read itNo winner. Three honest profiles, the cost of each over three years, and the specific questions that decide it.
Read itSix things we automated that saved real hours, two we built and deleted, and the rule we use to decide which is which.
Read itTell us what the site needs to do. You'll get a straight answer on scope, timeline and cost — usually inside one working day.
Rather write first?hello@quesiono.com— we reply within one business day.