← Back to posts

Faster issues, fewer round trips

Published: 8/2/2025

The issues pages were slow. Not mysteriously slow — slow in the specific, unglamorous way that comes from asking for things one at a time.

Two changes fixed it, and neither involved making anything faster. Both involved asking for less.

The library page was doing an N+1

/issues lists every repository we track alongside its issue counts. It got there by fetching the list of repository names, then fetching issue summaries for all of them, then stitching the two together in the page component — building a Map keyed on lowercased repo names, looking each one up, and filling in a hardcoded zero-counts object for anything that didn't match.

That stitching was about seventy lines of code whose entire job was to reassemble something the API had taken apart. Two round trips minimum, and the second one scaled with the number of repositories we track — which is the direction that number only ever moves.

The fix was to add getrepoissueslibrary on the Go API: one endpoint that returns repositories and their counts together, already joined. The page now makes one call and renders what comes back. The seventy lines are gone, along with the class of bug they carried — every name-normalization mismatch between the two lists silently rendered as a repo with zero open issues, which is a lie that looks exactly like a fact.

There's a fallback: if the endpoint 404s, because an older API build is deployed, the code drops back to the names-plus-batch-summaries path and logs a warning. Combined endpoints are the kind of change that's easy to ship on one side of a deployment and not the other.

The per-repo page was dynamic for no reason

/repos/owner/name/issues was marked force-dynamic, so every visit recomputed everything. For most repos that's merely wasteful. For something like llvm/llvm-project, with roughly fifty thousand issues, it's the whole page-load budget.

Nothing on that page needs to be fresh to the second. Issue analytics computed an hour ago are the same analytics. It's now on a one-hour revalidate, which means the expensive computation happens once and gets served from cache to everyone who arrives in the following hour.

The second half of the same change: the page was awaiting repository details, and only *then* fetching the issue summary and the first page of issues. Those three requests don't depend on each other. They're now a single Promise.all, so the page waits for the slowest one instead of the sum of all three.

Why this is a short post

There's no clever algorithm here. The library page was slow because it made a request per repository; the repo page was slow because it made three requests in sequence and cached none of them. The fixes are the obvious ones: return joined data from the API, run independent fetches concurrently, and cache what doesn't change minute to minute.

The reason it's worth writing down is that neither problem announced itself. Both pages worked correctly the entire time. force-dynamic was set early, when the page was cheap and the correctness of always-fresh data seemed free; it stayed set as the page got expensive. The N+1 arrived the same way — the endpoint that returned names existed, the endpoint that returned summaries existed, and joining them in the client was the shortest path to a working page.

Neither was a bad decision when it was made. They just stopped being good ones, quietly, and nothing in the system was going to raise its hand about it.

If you've got a page that's been slow long enough that you've stopped noticing, it's probably one of these two.