Orbit
English
Sign in
← Back to the blog
Engineering6 min read

We Made Orbit Do Too Much

Some of Orbit's performance problems are consequences of decisions we made. We let convenient shortcuts accumulate, gave ordinary actions too much work, and now have to clean up after ourselves. This is how that happens, what it costs, and why we have not earned a victory lap.

performanceengineeringdashboarddiscord

Some of Orbit's performance work is us cleaning up after ourselves. We built useful features, but we also built unnecessary work around them. The application then had to carry that work whenever somebody tried to use those features. We cannot take credit for the first part and blame an abstract technical problem for the second.

External services can be slow. Networks can be unreliable. A growing product has real engineering challenges. None of that explains why a page asks for information it does not need, or why the same information gets fetched twice. Those decisions have our fingerprints all over them. Calling them growing pains would be generous to us.

Convenient for us. Expensive for everyone else.

Loading a broad set of data makes a page convenient to build. Everything is available, another section can use the same state, and filtering can happen in the browser. The appeal is obvious. So is the missing question: how much of this does the person opening the page actually need? Our code contains too many places where the convenient answer won without earning it.

The Giveaway workspace gave us a fairly blunt example. Opening a list brought along collections that the list did not display. Opening a detail view brought along information for sections that had not been selected. The interface looked like one task. Underneath, we had volunteered it for several more. That was wasteful, and it was our implementation.

Every unnecessary read still needs to be processed, transferred, and held somewhere. The developer gets a convenient pile of data. The person using Orbit gets whatever delay and browser work come with it. We had made our own implementation easier by asking the application to do more. That is a poor trade, especially when the other side of it never got a say.

A tidy component does not make that trade better. Neither does a reusable helper with a sensible name. We can arrange unnecessary work very neatly. Some of our performance review has consisted of looking past that neatness and admitting that the work should never have been there.

The product grew into our shortcuts

A page that loads a whole collection ties the cost of opening that page to the size of the collection. More history means more work, even when the task is still just finding one record. A small dataset can make that decision look harmless. It does not make the decision sound. It only postpones the part where the cost becomes harder to ignore.

There does not have to be one spectacular mistake. An extra read here, an editor prepared before anyone opens it, another part of the interface requesting a refresh: each can look minor on its own. Put them together and ordinary actions acquire a workload that has very little to do with what was actually requested. We let that kind of work accumulate in the paths we are now revisiting.

Even code intended to reduce duplication could get in the way. In the shared Dashboard state, a request for newer information could wait for an older, obsolete read to finish. Sharing the read saved repetition, but the way we handled its lifetime could delay the refresh. We had solved one part of the problem and left the next part inside the solution.

A loading indicator is useful when something really is pending. It also makes it very easy to give all of this a familiar face and move on. The spinner tells you that Orbit is busy. It does not tell you whether Orbit has any good reason to be that busy. We cannot keep adding work to a click and then act surprised when the click takes longer.

More layers do not absolve us

Orbit has difficult responsibilities. Access to private information needs to be checked. Changes need to respect community boundaries. Actions must not be applied twice just because a request was retried. Those requirements remain. They deserve careful engineering, and they are not an excuse for unrelated overhead elsewhere in the same action.

It is too easy to point at a complicated system and assume its delays must also be complicated. Then a closer look finds unused data, repeated reads, or background work competing with a time-sensitive Discord response. The explanation gets much less impressive. We cannot use the difficult parts of Orbit to lend respectability to careless decisions in the ordinary parts.

Modularity helps us organize the product. It does not automatically keep an action small. The person pressing a button experiences every dependency we put in its way, regardless of how nicely those dependencies are separated in the repository. If a simple action needs several parts of the application to cooperate, we owe it a better reason than that the code happened to be arranged that way.

The repair is less flattering than the announcement

The current work starts with asking why an action does each piece of work at all. A queue needs enough records to browse, not its entire history in the browser. An unopened editor does not need to prepare itself in advance. A detail section can ask for its information when selected. That means following dependencies through the application and removing assumptions that had become part of the routine.

Some findings are almost embarrassingly mundane. Rendering a completed Giveaway's Discord card repeated a read of prize-delivery records and repeatedly requested the same status translation. The information did not become more correct because we obtained it again. We were making the application repeat itself. Removing that deserves a regression test; it does not deserve much applause.

Other changes require clearer ownership of work already in progress. A refresh needs a way to stop waiting for obsolete information. An initial Discord response needs room to proceed without unrelated background requests occupying its connections. We are revisiting those boundaries because the previous arrangements allowed avoidable interference. They were arrangements we put there.

Removing work still requires care. A total calculated from the first page can be quick and wrong. Loading a section later moves its wait to the first time it is opened. Refreshing an editor must not erase unfinished input. Getting the data flow right means accounting for those consequences too. Replacing one careless shortcut with another would be a depressingly predictable way for us to waste this effort.

We have not earned a victory lap

It would be convenient to call this a performance push, show the cleaner implementation, and consider the lesson learned. Convenient has already done enough damage here. Asking for only the data an action needs and avoiding duplicate work are basic responsibilities. Having to go back and enforce them across existing paths tells us something unflattering about how consistently we applied them.

The work has been checked locally. That gives us evidence for specific corrections; it does not establish a completed rollout or a measured production speedup. Real usage still has to judge the result, including larger histories, cold loads, and busy periods. We are not attaching an impressive percentage to a story whose evidence cannot support one. The code has supplied enough fiction about how much work a page needs.

The lasting correction has to reach the way we review features. A new capability brings reads, state, dependencies, and background work with it. Those costs need attention while we are building it. If we keep treating them as something to clean up afterwards, we will keep producing the same performance problem under different feature names. That would be repetition at a much less amusing level.

We want Orbit to do more useful things. We also need to stop making every useful thing drag so much unnecessary work behind it. That is the part we owe you, and it will take more than a better loading screen or a confident blog post. We built the features. We also built some of the waiting. Both belong in our account of Orbit.