We made our dashboard 35% faster without touching a single query
Our API answered in 40 ms. Our dashboard still took over a second to open. The difference was spent crossing the Atlantic, and four small changes won a third of it back.
Where the second went
Our servers are in Frankfurt and we're in Halifax. A round trip between them takes about 140 ms, and no code can make light go faster.
So the useful question isn't "how fast is each request?" but "how many times does the page cross the ocean?" Every request that waits for another one to finish pays the 140 ms again. Shaving 20 ms off a query saves 20 ms. Removing one crossing saves 140.
Measure with distance, not on localhost
On localhost a round trip costs nothing, so a staircase of requests looks fine. Add some distance:
- Chrome DevTools: in the Network tab's throttling menu, choose Add… and make a profile with 140 ms of latency and no bandwidth limit.
- Linux:
sudo tc qdisc add dev lo root netem delay 70msadds 70 ms each way. Undo it withsudo tc qdisc del dev lo root, or spend tomorrow wondering why everything is slow.
Then look for staircases in the waterfall: requests that only start once another finishes. We found four.
1. Compress the bundle
Our 311 KB JavaScript bundle went out uncompressed. Gzipped, it's 97 KB. A new connection only sends about 14 KB before waiting to hear it arrived (TCP slow start), so that's about two fewer round trips, not just fewer bytes.
The files are embedded in our Go binary and never change, so we compress them once at startup and serve those bytes to any browser that accepts gzip. Simplified:
var gzippedAssets = sync.OnceValue(func() map[string][]byte {
out := map[string][]byte{}
fs.WalkDir(distFS, "dist/app/assets", func(name string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
raw, _ := fs.ReadFile(distFS, name)
var b bytes.Buffer
zw, _ := gzip.NewWriterLevel(&b, gzip.BestCompression)
zw.Write(raw)
zw.Close()
out[name] = b.Bytes()
return nil
})
return out
})
Behind a CDN that's supposed to do this for you? Check that it does: curl -sI -H 'Accept-Encoding: gzip' https://your-site/app.js should show content-encoding: gzip.
2. Start independent requests together
The page fetched the Site's details, rendered, and only then did the dashboard ask for stats. But the stats only needed the Site's ID, which had been in the URL all along. Now both start at once:
const site = useAsync(() => api<Site>(`/api/sites/${siteId}`), [siteId])
useEffect(() => prefetchDashboard(`/api/sites/${siteId}`, query), [siteId])
When the dashboard mounts, its requests are already on their way, and it waits for those instead of sending duplicates. A parent fetches, renders a child, then the child fetches: it's the most common waterfall in single-page apps. If the child doesn't need the parent's answer, don't make it wait for one.
3. Don't ask again for what you just saw
Switching from "Last 30 days" to "Last 7 days" and back refetched everything. Now a view under a minute old shows instantly with no request, and an older one shows at once while it refreshes. That's The 30-line cache that made our dashboard feel instant.
4. One query, not one per Goal
The Goals panel queried Postgres once per Goal, one after another. Now it's a single query: 8 database trips down to 1.
The result
At 140 ms of latency, the demo dashboard's cold load went from 1.07 s to 0.70 s, and going back to a view you've already seen became instant. No query got faster. The page just stopped standing around waiting.
Open the demo, open DevTools, and watch the waterfall.