Cutting friction out of a mobile checkout
Buying gold in our app used to take more taps than it should have, and a stretch in the middle where the screen just sat there while a payment provider made up its mind. Nothing about it was broken. People completed purchases. But the drop-off between "opened the buy screen" and "confirmed the payment" was worse than it had any right to be, and no amount of staring at server timings explained it, because the server was fine.
The problem was that the app was fast and felt slow. Those are different problems with different fixes.
Measuring the thing that hurts
The first change was in what we tracked. We had endpoint latency, which told us the backend was healthy, and crash-free sessions, which told us the app wasn't falling over. Neither of those describes what a person experiences between opening a screen and finishing a purchase.
So we instrumented the funnel by step — screen opened, amount entered, payment method selected, provider handoff, confirmation — with timestamps at each. The picture that fell out was immediate and slightly embarrassing. Almost nobody dropped off at the steps we'd worried about. They dropped off during the provider handoff, in a window where the app showed a spinner and no other information for what could be several seconds.
People weren't leaving because it was slow. They were leaving because they didn't know if it was working, and when money is involved, uncertainty makes people back out.
Filling the silence
The fix wasn't making the provider faster — we don't control that. It was making the wait legible.
Instead of an indeterminate spinner, the handoff screen now says what's happening and roughly what to expect: contacting your bank, confirming the payment, updating your balance. Each stage advances as the actual state changes, driven by the same status polling that was already running. The total duration barely changed. The drop-off at that step fell substantially, because a progress indicator that names its steps is a promise that something is still happening.
The related change was making it safe to wait. If someone backgrounds the app mid-payment, they now come back to the same in-progress state rather than a blank buy screen, because the transaction state lives on the server and the app reconstructs from it on resume. Previously, coming back to a fresh screen made people assume the payment had failed, and some of them started it again — which was its own class of problem.
Removing taps that weren't buying anything
The other half was more ordinary. Every field on the buy screen got the same question: does the user have to make this choice, or are we asking because it was easier to ask than to decide?
Payment method defaulted to whatever they used last. Quantity got preset amounts alongside the free-text field, because most purchases cluster around a few round numbers. The confirmation step, which had been a full screen, became a sheet over the current context — so the transition costs nothing and going back doesn't feel like losing your place.
None of these are clever. Together they took a meaningful number of interactions out of the common path, and the common path is where almost everyone lives.
What I'd do differently
I'd instrument the funnel before touching anything. We spent time optimising a list render and a couple of API calls at the start of this work, on the reasonable-sounding theory that faster is better. It was faster. It changed nothing, because those weren't the moments where people were giving up.
The general version: performance work has two halves, and engineers are much better equipped for the first one. Making the code fast is measurable, satisfying, and has good tooling. Making the product feel fast is about what someone knows at each moment — whether they can tell it's working, whether they can tell how long is left, whether they believe their money is safe. A profiler will never show you that. Step-level funnel data will show you exactly where it hurts, and it's about an afternoon of work to add.