The fear here is a reasonable one. While you were building, the app felt instant. Then you launched, real people signed up, real data started piling in, and now the same pages take four seconds to load. Nothing changed in the code; the only thing that changed is that it’s being used.
I see this pattern constantly in apps built with Lovable, Bolt and Replit, and one thing is worth saying first. It’s almost never because the app was built badly. It’s because generated code optimises for working, not for scale. When you describe a feature to an AI, it writes the most direct code that gives the right result on the data in front of it, and with five rows that code is also fast. The gap between “direct” and “fast” only opens up once the data grows, and by then you have already shipped.
The good news is that the slow parts are predictable. There are about six of them, they show up in the same places every time, and you can find most of them yourself in an afternoon.
1. The N+1 query
This is the classic one, and it hides well.
Say you have a page that lists 50 orders, and next to each order you want to show the customer’s name. The fast way is one or two database queries: fetch the 50 orders, then fetch the customers for all of them in one go. The way generated code often does it’s different. It fetches the 50 orders in one query, then loops through them and runs a separate query for each order to fetch its customer. That’s 51 queries to draw one page. This is called an N+1 query, because for N rows you run N extra queries.
With five rows of test data, that’s six queries and it returns in a blink. With 5,000 rows on a busy page, it’s thousands of round trips to the database, and the page grinds. The behaviour never changed, the amount of data did.
To check it: turn on query logging for your database and load the page. If a single page view fires dozens or hundreds of near-identical queries that differ only by an ID, that is an N+1. The fix is to fetch the related data in one query instead of one per row.
2. Missing indexes
An index is like the index at the back of a book. Without it, to find every mention of a word, you read the whole book cover to cover. With it, you flip straight to the right pages.
Databases work the same way. When you ask for “all orders belonging to customer 88”, the database can either jump straight to them using an index, or re-read the entire orders table row by row to find the matches. Generated code rarely adds indexes, because on test data the difference is invisible. Reading a table of five rows is instant. Reading a table of half a million rows, on every request, isn’t.
To check it: turn on slow query logging. In Supabase or plain Postgres you can log any query that takes longer than, say, 500 milliseconds, then look at what shows up under realistic data. A query that reads a large table to find a handful of rows is usually missing an index on the column it filters by.
3. Loading too much data
Watch out for pages that fetch everything and then sort it out in the browser.
A common shape: the app asks the database for every row in a table, sends all of it to the browser as one large lump of JSON, and then the browser filters or paginates it into the ten rows you see. On test data this is fine, because everything is ten rows. In production the server hauls tens of thousands of rows out of the database and pushes a huge payload down the wire on every visit, so the page stalls before it can draw.
To check it: open your browser’s developer tools, go to the network tab, and reload a data-heavy page. If a single response is measured in megabytes, or thousands of records arrive to fill a short list, the app is loading too much. The fix is pagination and filtering on the server, so it only ever sends the rows the page needs.
4. First load and asset weight
Some slowness has nothing to do with the database. It’s the weight of what the page downloads before it can show anything.
The usual offenders are images and code. A photo taken on a modern phone can be several megabytes, and if it’s served at full resolution into a thumbnail, the browser downloads all of it to shrink it on screen. Add a large bundle of JavaScript and no compression, and the first impression is a blank screen for several seconds. That’s the most expensive kind of slow, because it happens before anyone has signed up. A slow first load loses people who would otherwise have stayed.
To check it: run the page through the network and performance tabs in your browser’s developer tools, and run a Lighthouse audit (built into Chrome, under the same developer tools). It will point straight at oversized images, uncompressed assets and heavy scripts, and tell you roughly what each is costing you.
5. Repeated expensive work that is never cached
Some numbers are costly to work out. A dashboard total that adds up every transaction. A report that crunches a month of data. A response from an AI API. If the app recomputes these from scratch on every request, then every visitor pays the full cost every time, even when the answer hasn’t changed since a minute ago.
Caching means working something out once, saving the result for a short while, and handing back the saved copy until it goes stale. Generated code tends to skip it, because on test data the work isn’t expensive yet.
To check it: for your slowest pages, ask whether the work behind them could be reused. If a total or an AI response is calculated fresh on every load but rarely changes between loads, it is a candidate for caching.
6. Slow work done in the middle of the request
Some jobs are slow by nature: sending an email, generating an export, calling an AI model. The question is whether the user has to wait.
If that work happens inline, in the middle of handling the request, the page hangs until it finishes. The user clicks “Save”, the app quietly sends three emails and calls an API, and the browser spins for eight seconds or times out. The right pattern is to do the quick part immediately, tell the user it worked, and move the slow part to the background where it can take its time without holding anyone up.
To check it: find every action that triggers an email, a file, or an external API call, and time it. If the user is waiting on the spinner while that happens, it belongs in a background job.
Where to start
Don’t try to guess which of these you have. Make them show themselves.
The single most useful thing you can do is test with realistic data. Take a copy of your app, seed it with tens of thousands of rows that look like real usage, turn on the slow query log, and click around like an ordinary user. The problems appear immediately, and in priority order, because the slowest, most-run queries rise to the top of the log. An afternoon of this tells you more than any amount of staring at the code.
From there, the fixes are ordinary: add the missing index, collapse the N+1, paginate the big list, cache the expensive total, push the slow job to the background. None of it means starting over, and none of it means the app was wrong. It was built to work, and now you’re making it hold up.
If you would rather have someone find all of this for you and hand you the list in order, that’s a large part of what I do. Performance is one section of the full launch checklist, and one of the easiest to get an early read on. Seed some real data, watch the slow query log, and you’ll know where your app is going to hurt.