Most performance problems in a Next.js + Supabase app are not about micro-optimizations — they come from doing work in the wrong place or at the wrong time: shipping JavaScript that should have stayed on the server, fetching data in series that could have run in parallel, or asking the database to scan a table it should have indexed. This guide covers the few decisions that matter most. The checklist at the end is generated from the enforceable rules, so it tracks exactly what the scanner checks.
Render on the server by default
The App Router makes Server Components the default for a reason: they render to HTML on the server and ship no JavaScript for that component to the client. Every component you can keep as a Server Component is bundle weight your users never download and hydration work their devices never do.
So the discipline is: server by default, client only when you need interactivity (state, effects, event handlers, browser APIs). And when you do need a client component, push the "use client" boundary as low as possible — wrap the small interactive leaf, not the whole page. A single "use client" near the top of the tree drags everything beneath it into the client bundle; the same directive on a tiny button does not.
Fetch data where the data lives
In a Server Component you can await a Supabase query directly. Do that instead of building an internal API route and fetching it over HTTP — the API route adds a network round-trip, a serialization pass, and a second place for bugs to hide, all to reach the same database you could have queried inline. Query Supabase directly in Server Components and skip the API hop.
Two more data-fetching mistakes dominate real apps:
- Serial waterfalls. If a page needs the user, their projects, and their notifications, and those three queries don't depend on each other, awaiting them one after another makes the page as slow as the sum of all three. Run independent fetches together with
Promise.all and the page is only as slow as the slowest one.
- Duplicate fetches. Don't fetch the same data in both a layout and the page it wraps. The layout and page render together; fetching twice doubles the database load for identical rows. Fetch once at the right level and pass it down (or rely on the framework's request-level cache).
One Supabase-specific gotcha worth internalizing: the query builder is immutable. Each chained method returns a new builder rather than mutating the old one, so a query you "set up" but never reassign silently runs without your filters — which is both a correctness and a performance bug (a missing .eq() can turn a point lookup into a full-table scan).
Make the database do less work
The fastest query is one the database can answer from an index instead of scanning. Add indexes on foreign keys and on the columns you commonly filter or sort by. A join across an unindexed foreign key, or a WHERE on an unindexed column, forces a sequential scan that gets linearly slower as the table grows — invisible in development with 50 rows, painful in production with 500,000.
Keep the interface responsive while data loads
Perceived performance is real performance to a user. Use route-level loading.tsx files so navigation shows an instant loading state and a Suspense boundary streams the rest in, rather than blocking on a slow query with a frozen screen. For data that genuinely needs to live and update on the client — real-time dashboards, anything that polls — reach for SWR or React Query rather than hand-rolled effects, so you get caching, deduplication, and revalidation for free instead of re-fetching on every render.
How to think about it
When a page feels slow, walk down this list in order before profiling anything exotic:
- Is interactive code shipping to the client that didn't need to? (Server Components, low
"use client" boundary.)
- Are independent fetches running in series instead of parallel? (
Promise.all.)
- Is the same data being fetched more than once? (Layout vs. page.)
- Is the database scanning instead of seeking? (Indexes on FKs and filters.)
- Does the UI block while it waits? (
loading.tsx, Suspense.)
Nine times out of ten the answer is one of those five, and each maps to an enforceable rule below.
The checklist beneath this article is derived from the rules this article references, so passing the scan and following this guide are the same exercise.