My App Got Slow the Moment It Had Real Data

SprintX Team

Written By

SprintX Team

AI & Product Engineering

August 05, 2026

8 min read

A dashboard loading slowly as the database grows

The specific reasons an app that flew on test data crawls on production data, how to find the slow query in an hour, and the fixes ranked by payoff.

For three months the app was instant. Then a customer imported their real dataset — 40,000 rows instead of the 50 you tested with — and the dashboard now takes eleven seconds, the search box freezes the tab, and the export endpoint times out.

Nothing changed in the code. That is the frustrating part, and it is also the clue. Performance problems in AI-generated apps are almost never gradual. They are cliffs, because the patterns a model generates are correct at small N and quadratic at large N. You do not slowly get slower. You are fine, and then you are not.

The good news: this class of problem has maybe six causes, they are all findable in an afternoon, and the fixes are usually a few lines each.

Measure before you touch anything

Guessing at performance is how people spend a week optimizing a component that accounts for 40 milliseconds. Get the number first.

Open the network tab on the slow page and sort by duration. You are looking for one of three shapes, and each points somewhere different:

  • One request taking 8 seconds. A database or API problem. Go to the query.
  • Two hundred requests taking 40ms each. An N+1 pattern in the frontend. Go to the data fetching.
  • Requests are fast, page is slow. A rendering problem. Go to the component tree.

Then get the server-side truth. In Postgres, run 'EXPLAIN ANALYZE' on the query behind the slow request — it tells you exactly whether the planner is doing a sequential scan and how many rows it touched. On Supabase, the dashboard's query performance view ranks your slowest statements by total time, which usually identifies the culprit in about two minutes. If you have no visibility at all, that is its own problem worth fixing first: adding observability turns future incidents from guesswork into a lookup.

The six causes

1. Missing indexes

The default. AI generators create tables with a primary key and nothing else, because the prompt was about features, not access patterns. Every filter and join then scans the whole table.

At 50 rows a sequential scan is faster than an index. At 50,000 it is 1,000 times slower, and the crossover happens without warning.

Index the columns you filter, join, and sort on — foreign keys first (they are not indexed automatically in Postgres), then tenant or organization ID, then created_at if you sort by it, then anything in a WHERE clause. Composite indexes should lead with the column you filter by equality. Do not index everything: each index slows writes and consumes storage, so index what your slow queries actually use.

This is usually a five-line migration that takes a page from eleven seconds to under 200 milliseconds. It is the highest-return change on this list by an order of magnitude.

2. N+1 queries

The signature AI data-fetching bug. Fetch a list of orders, then loop and fetch the customer for each one. Ten orders, eleven queries, nobody notices. Five hundred orders, 501 queries, the page dies.

It looks natural in generated code because each individual line is reasonable. Look for a data fetch inside a map, a component that fetches its own row, or a Supabase call inside a loop.

The fix is to fetch related data in one query — a join, a nested select, or an 'IN' query that batches the IDs — and pass it down. One request instead of five hundred.

3. Selecting everything, always

'select *' with no limit is the default an agent writes, and it means every query drags every column of every row across the network, including the large text fields and JSON blobs no screen displays.

Three fixes, all cheap: select only the columns you render, paginate every list endpoint (cursor pagination scales better than offset once you are past a few thousand rows), and never fetch a full table to compute a count — ask the database for the count.

4. Filtering and sorting in JavaScript

Related and worse. The app fetches all records and then filters, sorts, or aggregates them in the browser. This works beautifully on your seed data and turns into a several-megabyte download plus a frozen main thread on real data.

Any filter, sort, search, or aggregate should run in the database. The database is extremely good at it and it is the only participant that scales.

5. Unbounded joins and aggregate queries on the fly

Dashboards are the usual victim. A stats panel that computes sums across all history on every page load is fine at 1,000 rows and hopeless at a million. Either precompute — a materialized view refreshed on a schedule, or a rollup table updated on write — or cache the result. Most dashboard numbers do not need to be accurate to the second, and pretending they do is expensive. See caching strategy for how to decide what is cacheable.

6. Frontend rendering that never expected volume

If the requests are fast and the page still stalls, the problem is rendering. Common shapes: a table rendering 10,000 DOM rows with no virtualization, a component re-rendering the whole list on every keystroke because a new object is created each render, or an unthrottled search that fires a request per character.

Virtualize long lists, debounce search inputs, and memoize expensive derived values. Do those three and most "the UI is frozen" reports disappear.

Which fix to reach for

SymptomLikely causeFixTypical effort
One endpoint takes secondsMissing index or sequential scanAdd index, verify with EXPLAIN30 minutes
Hundreds of small requestsN+1 fetchingJoin or batch the query2–4 hours
Page downloads megabytesFetching all rows and columnsPaginate and select fieldsHalf a day
Search freezes the tabClient-side filteringMove filtering to the queryHalf a day
Dashboard slow, other pages fineLive aggregationPrecompute or cache1–2 days
Fast requests, slow pageRendering volumeVirtualize and memoize1 day
Slow only under concurrencyConnection pool exhaustionPooler, see belowHours

That last row deserves a note. If your app is fine for one user and collapses for ten, you are probably not slow at all — you are out of database connections, which is a different problem with a different fix. Serverless functions open a connection per invocation and exhaust the pool quickly; a pooling endpoint or PgBouncer solves it. Crashes with multiple users covers that path in detail.

Do this before your next customer, not after

Performance problems are trivially preventable and expensive to discover in production. Two habits cover most of it.

Seed realistically. Develop against a database with production-scale volume — a script that generates 100,000 rows takes twenty minutes to write and surfaces every one of the six causes above before a customer does.

Set a budget and watch it. Pick a number, say 500 milliseconds for any page-level request, and alert when a query exceeds it. This turns "the app feels slow" into a specific query with a timestamp. Monitoring for a small SaaS covers the minimum setup worth having.

If your app is already in production and getting slower every week, work in that order — measure, index, fix N+1, paginate — and re-measure after each step. The temptation is to do all six at once and then not know what helped. Broader diagnosis of a slowing app is covered in why your website is slow and slowdowns after adding features.

Frequently asked questions

Can I ask the AI to make my app faster? For a specific, isolated fix — "add an index on this column", "rewrite this loop as a single query" — yes, and it does it well. What it cannot do is tell you which query is slow, because that requires production measurements it has never seen. Bring it the EXPLAIN output and it becomes genuinely useful.

Do I need to upgrade my database plan? Rarely, and it is the most common wrong first move. A missing index makes a query 1,000 times slower; doubling your CPU makes it twice as fast. Fix the query first, and if it is still slow with a good plan, then consider hardware. See scaling a Supabase app for when the plan genuinely is the limit.

How much data before this starts to matter? Sequential scans usually become noticeable somewhere between 5,000 and 50,000 rows, depending on row width and query complexity. N+1 patterns hurt much earlier — often at a few hundred records. If any customer of yours has more than a thousand rows in a table you filter on, assume you are already paying for it.


If your app slowed down the week it got real users, the cause is usually three or four specific queries rather than an architecture problem. SprintX profiles the live app, fixes the queries and the fetch patterns, and leaves you with the monitoring to catch the next one before a customer does. Send us your slow page.

Related Articles

Contact us

to find out how this model can streamline your business!