Problem:
Some time ago I was investigating the case where data shown in one dropdown loaded almost 5 seconds. It took unacceptably long time giving that dropdown contained (no to so long) list of names. Where to start? First check – network tab. When modal was opened 4 requests was sent, 2 OPTIONS
and 2 GET
requests. As you can see total response time was above 4.5 sec.
It seemed strange to me, because functionality of modal was supposed to ask for data from backend only once.
Investigation:
I started debugging on setting breakpoint on backend WebAPI controller method responsible for providing companies. It was responding for 2 GET
requests. Strange… So problem was on front’end part of the app.
Quick look into the front’end code did not catch my attention to anything specific (simplified code below).
Because we needed both: types and companies on our modal at one time, they were putted into forkJoin
which will wait till last elements for all observables will be returned. Details regarding forkJoin
here: https://www.learnrxjs.io/operators/combination/forkjoin.html
ngAfterViewInit(): void { this.typeOptions$ = this.getTypes(); this.companies$ = this.getCompanies(); forkJoin(this.typeOptions$, this.companies$) .pipe(takeWhile(() => this.isTrueParam)) .subscribe( ([typeOptions, companies]) =>{ //processing the response }) getCompanies(): Observable<FormSelectOption[]> { return this.companyService.getCompaniesList() .map((src) => { //response mapping }); }
component code was calling simple service, which in turn called backend (but we are covered there).
getCompaniesList(): Observable<CompaniesList[]> { return this.httpClient.get<CompaniesList[]>(endpoint); }
In the template we were asynchronously receiving observable of companies and assigning it to options of dropdown.
<form *ngIf="!isLoading" [formGroup]="form" (submit)="save($event)" (reset)="cancel()"> <div class="row"> <div class="col-sm-12"> <dropdown-select [formInstance]="editForm" [required]="'required'" [options]="companies$ | async"> </dropdown-select> //similar dropdown for types </div> </form>
At first glance everything looked just fine.
I was scratching my head for a while, having no idea why this component is acting that way. To investigate further, I set the debugger in the front’end service in the line before the call to WebApi.
getCompaniesList(): Observable<CompaniesList[]> { debugger; return this.httpClient.get<CompaniesList[]>(endpoint); }
Suprisingly that breakpoint was hit only once. But hey, we had 2 hits on backend Web API ! Now I knew that the problem was for sure somewhere in component/template
code. I started reading code next time and decided to read about both forkJoin
construction and async pipe
in the template.
I read about forkJoin
:
One common use case for this is if you wish to issue multiple requests on page load (or some other event) and only want to take action when a response has been received for all.
https://www.learnrxjs.io/operators/combination/forkjoin.html
Ok that was clear.
Next that async pipe
:
The asyncpipe subscribes to an Observable or Promise and returns the latest value it has emitted.
https://angular.io/api/common/AsyncPipe
Aha moment ! “pipe subscribes to”… Now everything was clear, this inconspicuous pipe created second subscriber, which was calling backend behind the scenes.
How to fix it?
So the fix was easy, just deleting async pipe from template.
<form *ngIf="!isLoading" [formGroup]="form" (submit)="save($event)" (reset)="cancel()"> <div class="row"> <div class="col-sm-12"> <dropdown-select [formInstance]="editForm" [required]="'required'" [options]="companies$"> </dropdown-select> //similar dropdown for types </div> </form>
Alternatively you can use share()
operator of RxJS
, which allows you to share one observable between multiple subscribers. https://www.learnrxjs.io/operators/multicasting/share.html
How much we earned?
Let’s check how much time we earned !
Around 1,5 sec, nice !
But still 3 seconds for such request seemed to be little bit to long… Let’s look into timing tab of google chrome dev tools:
We can see here interesting thing. Between the others, there are two times in this list:
Waiting (TTFB). The browser is waiting for the first byte of a response. TTFB stands for Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response.
Content Download. The browser is receiving the response.
https://developers.google.com/web/tools/chrome-devtools/network/reference#timing-explanation
as we can see in screenshot, TTFB is 2.1 secs, however downloading the response is only 7.63 ms. Thanks to this information I know that issue is somehow connected with processing data on the backend. So, I moved to diagnosing the backend without worrying about frontend performance anymore 🙂