A dashboard does not have to load in one gulp
A dashboard that takes several seconds to appear is not a slow page. It is a page people stop opening. They learn to export a spreadsheet, or they ask someone else for the number, and the product you spent months on becomes a backup plan.
I ran into that on a cloud cost product. The screen had to show spend, anomalies, and recommendations. The first version asked for too much of it at once. One request gathered a wide tree of data, the server did a lot of work before it could answer, and the browser sat there until the whole thing arrived. On a good day it was a few seconds. On a heavy account it was long enough that you wondered if the request had died.
The tempting fix is to make the query faster and leave the shape alone. Sometimes that is the whole job. An index, a narrower select, a total that was being computed in the application when the database could have done it. I did that work. It was necessary, and it was not enough. The page was still one gulp. If any part of the gulp was slow, the entire screen waited. A person who only wanted the top-line number paid for the recommendations they had not asked to see yet.
So I split the page into sections that could load on their own. Each section had its own GraphQL query, with the fields that section actually rendered. The summary could come back while a heavier breakdown was still running. A failure in one panel did not blank the rest of the screen. The user saw something true immediately, and the rest filled in.
That sounds like a frontend trick. The useful part was the contract. Once a section owned its query, the backend could serve that question without assembling a document nobody needed. Heavy joins stayed with the panel that required them. Shared numbers were not copied into three payloads “just in case.” The React side got simpler too. A section could have its own loading and error state, which is a more honest interface than a spinner over the whole page.
There is a cost. More requests means more chances to get the loading order wrong, and a chatty page can be its own kind of slowness if you are careless. I kept the split at the level of things a person looks at separately: the number they came for, then the detail they might open. I did not make a request per table cell. The point was to match the fetch to the way the screen is read, not to spray the network.
The shared UI around those sections mattered as much as the queries. When several micro-frontends need the same table, the same empty state, and the same way of asking for an export, you do not want each one to invent it. A slow dashboard that is also inconsistent is two problems. I treated the shared package as part of the performance work. If every screen fetches and renders the same way, the next slow page is easier to split, because the pattern is already there.
The result was a screen that felt near-instant for the part people were waiting on, instead of a multi-second wait for a document they would only half read. I have reused the same move since. If a page is slow, I ask what the person is actually waiting for, and I let that arrive first. Everything else can catch up.