Why your app slows down as it fills up

At the start, everything is fast. The app opens instantly, lists scroll, nothing stutters. Six months later the same screens take two seconds to appear and your users report things that “don’t always show up.”
Nobody slowed the code down in between. What changed is the amount of data it runs through. And the real question isn’t “how many users can this app handle,” it’s what, inside this app, costs more with every item added.
On 16 September 2026 we opened the code of BeeDone, one of our own apps, to answer exactly that question on a single screen: the list of today’s tasks. This article covers what that audit found, because the three mechanisms behind it are the most common ones on the market, and none of them requires knowing how to code to be spotted.
A “random” bug almost never is
The reported symptom was this: you accept a shared task, the person’s photo doesn’t appear. You tick an unrelated task, and the photo appears. The kind of behaviour that gets filed as a “random display bug” and never reproduces in front of the developer.
There was nothing random about it. A line of code written two years earlier compared each new version of a task against the previous one and decided whether the screen was worth refreshing. That comparison covered 6 pieces of information out of the 21 a task carries. Any update touching only the other 15 was discarded. Not delayed: discarded. It only came back on the next change, hitching a ride.
That filter had been written to make the app faster. It is today the only place in that app capable of losing data without anything flagging it.
That’s the first lesson, and it’s an uncomfortable one: an optimisation placed in the wrong spot isn’t merely useless, it becomes a source of bugs nobody thinks to look for there. It’s invisible in acceptance testing, because acceptance testing exercises one task, not fifty, and it exercises what changes, not what doesn’t.
The three things that grow with your data
An app is slow when it does work proportional to what it holds. The audit found only three families of cost on that screen. There are rarely more, and they turn up almost everywhere.
| The mechanism | What happens | What the user feels |
|---|---|---|
| One connection per item | Every row in the list opens its own connection to the database, and never closes it when leaving the screen | The app heats up, drains battery, and gets slower after a few round trips |
| Everything drawn, even off screen | The list builds all 200 rows while the screen shows twelve, images included | Opening the screen takes longer and longer, in proportion to the content |
| One change redraws everything | A detail changes on the user profile, and all 50 rows redraw | Stutter while scrolling, animations that skip |
What the three have in common: they are painless at launch. With five tasks in the database, five connections and five drawn rows don’t show. The cost exists from day one, it simply isn’t visible yet.
What the audit counted
Here are the counts recorded on that screen, assuming a well-filled account: 50 pending tasks and 20 habits.
| What is counted | Observed | Target | What changes |
|---|---|---|---|
| Open database connections | 24 | 5 | One shared connection instead of one per item |
| Connections closed on leaving the screen | 0 | 0 | Accepted on both sides, but bounded in the target |
| Rows built on opening | 50 | ~12 | Only what’s on screen gets built |
| Rows redrawn when the profile changes | 50 | 0 | Compared by content, not by identity |
| Rows redrawn when one task is edited | 50 | 1 | Each row only watches its own task |
| Task information reaching the screen | 6 of 21 | 21 of 21 | Removal of the filter described above |
| Connections left behind after 20 screen round trips | 20 | 0 | Closed on leaving the screen |
One clarification that matters more than the numbers themselves. These counts come from reading the code, not from measuring on a phone. They are structural counts, and they must be confirmed by automatic counters before being claimed as results. In our own tracking they stay marked “to confirm” until those counters have run.
This is a distinction you can demand from any supplier: a number deduced from code and a number measured on a device are not presented the same way. When you’re told “three times faster” without being told which of the two it is, the answer teaches you more than the number.
Why the fix isn’t “optimise harder”
Faced with those numbers, the reflex is to add safeguards. That’s exactly what produced the bug at the top of this article.
The fix goes the other way: you remove the filter, and you get the same saving lower down and more finely. Each row now watches only its own task. One edit on a task redraws one row, not fifty. An edit on nothing redraws nothing, because two identical versions are recognised as identical.
The difference is structural. A filter placed high up decides for everyone and gets it wrong for everyone. A rule placed at each row can only get it wrong for that row.
It’s the same logic described in how to tell whether your app is maintainable: what makes a fix safe isn’t the number of precautions, it’s the fact that a change stays local.
How you prove a screen is reactive
“It’s reactive” isn’t a verifiable sentence. It turns into tests that fail before the fix and pass after. On this screen they were written before touching the code:
| The test | What it checks | Starting state |
|---|---|---|
| 21 pieces of information, 21 cases | Every changed piece makes the row move | 15 failing |
| Connection counter | How many connections open, and close | 1 per item |
| Redraw counter | How many rows move when the profile changes | All 50 |
| 200-task load | How many rows actually get built | All 200 |
| 20 screen round trips | How many connections are left behind | 20 |
| Compared screenshots | 3 screen widths, light and dark mode, doubled text size | New |
The last row deserves a word. The last three visual defects in this app were all caught by a screenshot looked at by eye, never by an automatic test: an element overflowing by 43 pixels, text cut off at 200% size, a drawing rendered twice. An automatic test checks what you thought to ask it. A screenshot shows you what you weren’t thinking about.
And an honest limit: none of these tests measures actual smoothness. That requires a reading on a real device, in production conditions, before and after. Until that reading exists, the smoothness gain is a hypothesis.
What this fix does not solve
The audit identified a problem it doesn’t fix: the app still loads an entire list in one go. One screen caps at 50 items with no way to see more, another has no limit at all. Fixing that touches how the app talks to its database, so it’s a separate piece of work, quoted separately.
The risk at that precise moment isn’t technical. It’s in the vocabulary. On delivery day it would be easy to announce that “list performance is sorted.” It won’t be. What will be sorted is the work the screen does. Not how much data it fetches.
A supplier who names what’s left to do costs you less than a supplier who lets you find out. It’s one of the few reliable signals before signing, and it requires no technical skill: just notice whether the sentence “here’s what this doesn’t cover” gets said without you having to ask for it.
The four questions to ask
They work on an agency, on an in-house developer, or during acceptance testing before signing off a delivery.
- “What, on this screen, costs more with every item added?” A correct answer names two or three precise mechanisms. A vague answer means nobody counted.
- “That number, was it read in the code or measured on a phone?” Both are useful. Confusing them is not.
- “How do you know this screen displays every update?” If there’s a test per displayed piece of information, it’s verified. Otherwise it’s hoped for.
- “What does this fix not solve?” If the answer is “nothing,” the question hasn’t been worked on.
These four questions cost nothing to ask and take minutes to answer. The scoping questionnaire covers them, and the scope of a delivered app is detailed on the mobile app page.
In short
- An app doesn’t slow down because of user count, but because part of its work is proportional to what it holds.
- Three mechanisms explain it almost everywhere: one connection per item, everything drawn even off screen, one change redrawing everything.
- A “random” display bug is almost always deterministic. In the BeeDone audit of 16 September 2026, an optimisation filter let through 6 pieces of information out of the 21 a task carries.
- An optimisation placed too high decides for everyone and gets it wrong for everyone. The right fix was to remove that filter, not add another.
- Counted on that single screen: 24 open connections, 50 rows built for a screen showing twelve, 20 connections left behind after twenty round trips. Counts read from code, to be confirmed by automatic counters.
- “It’s reactive” is proven by tests that fail before and pass after, plus screenshots looked at by eye: the last three visual defects were all caught that way.
- No automatic test measures actual smoothness. That requires a reading on a real device, before and after.
- Four questions are enough at acceptance: what costs per item, number read or measured, how you know everything displays, and what the fix doesn’t solve.