AI tools write code that works on a demo with ten rows of data. Then real users arrive, the tables grow, and pages that loaded instantly start taking seconds. The app isn't broken. It's doing far more work than it needs to.
The good news is that slowness in AI-built apps usually comes from the same handful of causes. Fix them in the right order and most apps get dramatically faster without a rewrite or a bigger server.
Measure first
Guessing wastes time. Spend an hour finding out where the time actually goes.
- Open your browser's developer tools, go to the Network tab and reload the slow page. Sort by time and by size.
- Run Lighthouse or PageSpeed Insights on your key pages to see load metrics and the biggest offenders.
- Check your database's slow query log or query performance view. Supabase and most hosted Postgres providers include one.
- Count the requests. A single page making dozens of API or database calls is a strong clue.
N+1 queries
This is the most common cause by far. The code loads a list of items with one query, then makes another query for each item to get related data. Fifty orders means fifty-one round trips to the database. It's invisible with five test records and painful with five hundred.
The fix is to fetch related data in one go, with a join or a single query for all the IDs at once.
// Slow: one query per order
for (const order of orders) {
order.customer = await db.customer.findUnique({ where: { id: order.customerId } });
}
// Fast: one query for all customers
const ids = orders.map((o) => o.customerId);
const customers = await db.customer.findMany({ where: { id: { in: ids } } });With Supabase, you can usually ask for related rows in the same select, such as select("*, customer(*)"), instead of looping.
Missing indexes
Without an index, the database reads every row in a table to find the ones you asked for. That's fine at a thousand rows and slow at a million. Columns you filter, join or sort by need indexes, especially foreign keys like user_id.
create index orders_user_id_idx on public.orders (user_id);
create index orders_created_at_idx on public.orders (created_at desc);Use explain analyze on a slow query to see whether it's scanning the whole table. Don't index every column, though: each index slows down writes a little and takes space.
Overfetching
Generated code often loads everything and filters in the browser: every row, every column, every time. Push that work to the database instead.
- Select only the columns the page shows, not select("*").
- Filter and sort in the query, not in JavaScript after the data arrives.
- Paginate long lists. Load twenty or fifty rows, then more on demand.
- Count on the server. Don't download a whole table just to show its length.
- Stop refetching the same data on every render or tab switch.
Images and bundle size
Even with a fast backend, a page feels slow if it ships megabytes to the browser. Two culprits dominate.
Images: phone photos uploaded at full resolution and displayed as thumbnails. Resize on upload or use an image service, serve modern formats such as WebP or AVIF, set width and height so the layout doesn't jump, and lazy-load anything below the fold. Frameworks like Next.js include an image component that handles much of this.
JavaScript bundle: AI tools add libraries freely, sometimes several that do the same job. Run a bundle analyzer, remove unused dependencies, replace heavy libraries with lighter ones, and load rarely used parts such as charts, editors or admin screens only when needed.
Caching
Many apps compute the same answer thousands of times a day. Caching stores it once.
- Serve static assets and public pages through a CDN with sensible cache headers.
- Cache expensive results, such as dashboards or reports, for a few minutes instead of recalculating per visit.
- Use your framework's data caching where content doesn't change per user.
- Be careful with anything personal. Never cache one user's data where another user could receive it.
What order to fix things in
- N+1 queries and missing indexes, because they get worse as you grow.
- Overfetching and pagination, because they cut both load time and hosting bills.
- Images, because they're often the single biggest download.
- Bundle size, for pages where first load matters most, like your landing page and signup.
- Caching, once the underlying work is already efficient.
Upgrading to a bigger server is tempting, but it only hides these problems for a while and you pay for it every month. Fixing the cause is usually cheaper and lasts.
Performance is part of every Tech Check: we find the slow queries and heavy pages in your app and price the fixes, so you know which ones are worth doing first.