// 08Insight
The same request, five times
A shared RxJS stream cut API consumption on a government case system by about 40%. No component logic changed.
· Rabin R
The complaint was slow page loads. On the Fiji immigration internal system — the application officers use to assess visa and permit cases — opening a single case record took long enough that people noticed, and noticing is the threshold that matters on software somebody uses four hundred times a week.
Slow page loads have an obvious suspect, so we checked it first and it was innocent. The bundle was not the problem. Lazy loading was already in place. The screen was not rendering an unreasonable number of components. What the network tab showed instead was the same three reference endpoints — country list, visa categories, office locations — being requested five and six times on a single navigation.
Why a good decision produced a bad outcome
The cause was not carelessness. It was a reasonable rule applied consistently. Each panel on the case screen — applicant details, document checklist, assessment history, routing — had been built to be self-sufficient: fetch what you need in ngOnInit, do not assume a parent has already loaded it. That rule is what lets panels be reordered, reused on other screens and tested alone.
It also means that when four panels each need the country list, the country list is fetched four times. The duplication was structural. Nothing in any single file looked wrong, which is exactly why it had survived review for a year.
// Repeated, near-identically, in every panel on the screen.
export class DocumentChecklistComponent implements OnInit {
countries: Country[] = [];
constructor(private readonly api: ReferenceApi) {}
ngOnInit(): void {
this.api.getCountries().subscribe((list) => (this.countries = list));
}
}The fix: one stream, shared
The obvious repair — lift the fetch into the parent and pass it down — would have undone the property that made the panels reusable. The better repair leaves every component exactly as written and changes what the service does underneath them.
@Injectable({ providedIn: 'root' })
export class ReferenceApi {
private readonly countries$ = this.http.get<Country[]>('/api/reference/countries').pipe(
// refCount: false keeps the value after the last panel unsubscribes, so
// navigating back to the screen does not re-fetch it.
shareReplay({ bufferSize: 1, refCount: false }),
);
constructor(private readonly http: HttpClient) {}
getCountries(): Observable<Country[]> {
return this.countries$;
}
}Four subscribers, one HTTP request, and — because the observable is created once on the service rather than per call — a second navigation to the screen makes no request at all. Not one component changed. The panels still fetch what they need in ngOnInit and still work in isolation, which was the point of building them that way.
The part that is actually hard
Caching reference data is easy. Deciding when the cache is wrong is not. A cached list that changes underneath you is a bug that reaches production quietly, weeks later, as a question about why the dropdown does not show the new office.
So the cache is invalidated deliberately rather than expired on a timer. The workflow events that can change reference data — an administrator editing a category, a new office being registered — clear the relevant stream, and everything downstream re-fetches on next subscribe. A timeout would have been less code, and would have meant either stale data for its duration or pointless requests forever.
private countriesCache$?: Observable<Country[]>;
getCountries(): Observable<Country[]> {
this.countriesCache$ ??= this.http
.get<Country[]>('/api/reference/countries')
.pipe(shareReplay({ bufferSize: 1, refCount: false }));
return this.countriesCache$;
}
invalidateCountries(): void {
this.countriesCache$ = undefined;
}What it was worth
API consumption on the case workflow screens dropped by approximately 40%, and frontend load time by roughly half. The second number is the one that mattered to the people using it: on a system somebody works in all day, a delay repeated across hundreds of case screens is not a metric, it is hours of a working week.
The change was around thirty lines across two services. That ratio is typical of this work, and it is the reason I measure before touching anything — the expensive fix is rarely the invasive one, and the invasive one is rarely the fix.
The lesson worth keeping
The instinct when a screen is slow is to look at rendering, because rendering is what you can see. In four years of this the cause has more often been the data layer doing something reasonable too many times. Before optimising anything, sort the network tab by name and count the duplicates. A URL that appears more than once per navigation is work that costs nothing to remove.
If your network tab shows the same endpoint several times per screen, that is usually a data-layer problem rather than a slow API. I do scoped performance investigations that say which of the four usual causes you actually have before anyone writes a fix.
