Why your vibe coded app is slow once real data arrives
I built an internal dashboard a while back that was instant for weeks. Every list painted the moment I clicked it, the demo was clean, and I moved on. Then a real account loaded in, with real volume and real attachments, and the same screen that used to appear immediately sat there for eleven seconds and then gave up. Nothing had errored. Nothing had been rewritten. The code that flew last week was the code that hung this week, and the only thing that changed was that the data got real.
If your vibe coded app is fast in testing and slow once real data arrives, the cause is almost always that a list endpoint is shipping the whole record when the list only needs a few fields of it, and you never saw it because you tested against a handful of seed rows where sending too much costs nothing. Speed on an empty dataset is not a property of the code. It is a property of the emptiness. The generator wrote something that worked, and worked is not the same as sized.
why is my app fast in testing and slow in production?
Because you validated it in the one environment that could not show you the problem. A seed dataset is five rows you typed in to see the screen render. Five rows of anything is fast, even five rows of the worst possible query, because five of a bad thing is still five. The moment that matters is the fiftieth row, or the five-thousandth, and that moment lives in production, on a real account, where you were not looking.
This is the same trap as any bug that only shows up with real inputs, and it is worth naming as a principle because it repeats: verify in the real environment, not the friendly one. For a browser bug that means a real device instead of your desktop. For a data bug it means real-shaped, real-volume data instead of the seed set you built the UI against. Vibe coding makes this sharper, not softer, because the agent optimizes for the thing you can see it produce, which is a working screen over the data on hand, and it has no reason to worry about the data you have not given it yet. The happy path it builds is the happy path you demoed, and the demo dataset is the smallest, cleanest data your app will ever hold.
what actually makes the list slow?
Look at what the endpoint sends on the wire, not at what the screen shows. Those are two very different sizes, and the gap between them is where the seconds go.
The receipt that taught me this was a list view that had no business being slow. The database was fine. The query was fine. The list on screen showed a name and a status and a date per row, three small fields. But the endpoint feeding it was returning the full record for every row, every column the table had, and one of those columns carried the item's imagery inline as base64 text. Fifty real rows moved roughly 750 megabytes over the wire to render a list that displayed about a kilobyte of visible text. The browser asked for a summary and got fifty complete objects with their images stuffed inside, and it choked trying to hold all of it. The fix was not a bigger server or a faster database. It was a lightweight response shape, a projection that returned only the three fields the list actually renders, and it dropped the payload from around 750MB to about 500KB. Same data source, same screen, four orders of magnitude less shipped.
That fat payload has a second face I have written about from the cost side, because the same megabytes that hang the dashboard also run up the bill on every call. Latency and cost are the same disease measured with two different instruments: a payload that nobody sized, riding every request. The dashboard shows you the symptom in seconds. The invoice shows you the same symptom in dollars. Neither one shows up until the data is real.
The reason a generator reaches for the whole record is that it is the convenient default. A list query that returns everything is one line. A query that returns exactly the three columns the view needs is a decision, and the agent had no reason to make that decision when five seed rows made both versions feel identical. So it shipped the convenient one, and convenient stayed invisible right up until the meter, or the clock, turned it into a number.
how do I test for this before a customer finds it?
Seed with data that looks like production, then measure the size of your highest-frequency call before you trust it.
The cheapest habit that would have caught this: pick the endpoint your app hits most, usually a list or a dashboard load, and log the actual byte size of its response, not the number of rows, the bytes. Then feed it a dataset that resembles what a real account will hold, not the tidy five rows you built the UI against. Real volume, real-length text, real attachments, the messy record with every optional field filled in. If the response balloons when the data gets real, you have found the thing that will hang in front of a customer, and you found it on your own machine where fixing it is free.
This is what AI orchestration discipline actually buys you, and it is the same instinct SpecMesh applies everywhere: the spec for an endpoint should name what it returns, so a fat default never becomes a standing charge. When you say up front that a list endpoint returns three named fields and nothing else, the generator has a contract to build against instead of a convenient default to reach for. Write down what the response is supposed to contain, and at what scale it has to hold up, before you generate the thing that produces it. A payload you never specified is a payload nobody is accountable for.
what tends to break with this
The list endpoint returning full objects is the most common one, but it rarely travels alone. Watch for a detail-shaped query feeding a summary view, the whole row where a card needs a headline. Watch for images or files served inline inside a list instead of as separate lazy-loaded requests. Watch for the endpoint that fetches every row and paginates in the browser, so the page pulls ten thousand records to show twenty. Watch for a screen that renders every row into the DOM at once, no windowing, so even a lean payload dies on a long list. Each of these is invisible at seed scale and each one has the same tell: the response, or the render, grows with the data instead of staying flat.
The deeper pattern is that a generator applies its shortcuts uniformly, so a fat list endpoint in one place is usually a fat list endpoint in every place the agent built the same kind of screen. Finding one is a reason to go count the others before they each find their own real account. The crash you can see is a sample, not the population.
questions that keep coming up
Is this a database problem? Usually not. The database returning fifty rows quickly is normal. The problem is what your endpoint does with those rows before it sends them, and how much of each one it decides to include. Look at the response payload before you tune the query.
Do I need a bigger server? Almost never, and reaching for one hides the bug instead of fixing it. A bigger server ships the same 750MB slightly faster and bills you more to do it. Size the payload down and a small server is plenty.
Why did it pass every test? Because the tests ran on seed data, and at seed scale the slow version and the fast version are indistinguishable. A test that never runs against real-shaped volume cannot see a problem that only exists at volume. That is the whole argument for verifying in the environment your users are actually in.
If you are a vibe coder shipping an app that is quick for you and you are not sure it will stay quick for a real customer, /work-with-us. Send me the build and the screen you are worried about, and I can run a diagnostic pass over what your endpoints actually ship at real volume, find the payloads the generator never sized, and set the projections that keep the app fast when the data stops being a demo. Custom app work starts by measuring the real thing, because the app that only looks fast will keep looking that way right up until someone real logs in.
The dashboard was never fast. It was empty, and empty is a state your app is only in once.
// part of the custom apps topic
// grab the free starter kit that makes your AI stop forgetting and stop guessing: get it →
// building with AI? the field manual has the structured lessons.
// hitting this on a real build? this is what I fix →