Every screen in Cleo's dashboard is rendered on the server before it reaches the browser. The server reads what the screen needs from the database, builds the page, and sends it. At the start of October I measured how long that took on the live product, opening each screen cold, warm, and by navigation, at desk size and at phone size.
Most screens took between one and a half and nearly three seconds to first paint. Settings took more than five.
Distance multiplied by count
The database is in Sydney, close to most of the people who use Cleo. The functions that render pages were running in the hosting platform's default region, on the east coast of the United States. Each read from the database was a round trip across the Pacific: around two hundred milliseconds before any work was done.
Two hundred milliseconds is tolerable once. A screen did not make one read. Between authentication, the user's profile, the workspace, the layout's own data and the page's data, a typical screen made eight to ten reads, many of them in sequence, because each depended on the one before or had simply been written that way. Sequential reads multiply latency. Ten of them is two seconds of waiting on the speed of light.
Fewer trips first
I fixed the code before moving anything, for two reasons. Fewer round trips help wherever the functions run. And if I had moved the functions first, the improvement would have hidden the waste.
Each server render now verifies the user and reads their profile once, shared between the layout and the page through React's request-scoped cache. Before, the layout and the page each did their own, across forty-seven pages. The layout's reads go out in two overlapping rounds instead of five in a row. The chat home reuses the layout's list of recent conversations instead of fetching it again. Settings sends its reads together. The media screen folds its reel status into the library reads.
Along the way, a check for whether a new user needed to create a workspace turned out to be a public server action that nothing on the client ever called. It is now a server-only function that takes the already-verified user. A public endpoint nobody uses is still a public endpoint.
With those changes live, still rendering from the United States, screens painted in about one and a quarter to just under two seconds. Settings came down to about two and a half. Better, and still slow.
One line
Then the functions moved to Sydney, beside the database. It was one line of configuration, and removing that line moves them back.
The same walk afterwards: most screens painted in three to seven hundred milliseconds, Settings included. The heaviest screen, the media library, took around a second. Several screens became usable in under six hundred milliseconds where they had taken over two seconds that morning.
What I would do differently
The region should have been set on the first day. A hosting platform's default region is a default for the platform's convenience, not a decision about your users. If your database lives in one place, your functions should live beside it, and if most of your users are far from both, that is a separate decision worth making deliberately.
The order still seems right to me, though. Reduce the number of round trips, then shorten each one. Shortening alone would have made the waste cheaper and left it in place. The script that produced these readings lives with the code, so the next time a screen feels slow there is a before to compare against.