← Back to blog
startups

Why your app slows down as it fills up

By Youcef EL KAMEL
8 min read

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 mechanismWhat happensWhat the user feels
One connection per itemEvery row in the list opens its own connection to the database, and never closes it when leaving the screenThe app heats up, drains battery, and gets slower after a few round trips
Everything drawn, even off screenThe list builds all 200 rows while the screen shows twelve, images includedOpening the screen takes longer and longer, in proportion to the content
One change redraws everythingA detail changes on the user profile, and all 50 rows redrawStutter 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 countedObservedTargetWhat changes
Open database connections245One shared connection instead of one per item
Connections closed on leaving the screen00Accepted on both sides, but bounded in the target
Rows built on opening50~12Only what’s on screen gets built
Rows redrawn when the profile changes500Compared by content, not by identity
Rows redrawn when one task is edited501Each row only watches its own task
Task information reaching the screen6 of 2121 of 21Removal of the filter described above
Connections left behind after 20 screen round trips200Closed 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 testWhat it checksStarting state
21 pieces of information, 21 casesEvery changed piece makes the row move15 failing
Connection counterHow many connections open, and close1 per item
Redraw counterHow many rows move when the profile changesAll 50
200-task loadHow many rows actually get builtAll 200
20 screen round tripsHow many connections are left behind20
Compared screenshots3 screen widths, light and dark mode, doubled text sizeNew

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.

  1. “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.
  2. “That number, was it read in the code or measured on a phone?” Both are useful. Confusing them is not.
  3. “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.
  4. “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.
#slow mobile app #why is my app slow #mobile app performance #random display bug #app performance audit #app scalability #premature optimization #mobile app acceptance testing #taking over an app