Skip to content

Commit

Permalink
fix(http): Run fetch request out the angular zone (#50981)
Browse files Browse the repository at this point in the history
Having the request run in the angular zone has the consequence of triggering the CD for every read of the response stream.

This commit wraps the whole `doRequest` to run outside angular with every callback on the observer being called inside the zone.

Fixes #50979.

PR Close #50981
  • Loading branch information
JeanMeche authored and thePunderWoman committed Jul 17, 2023
1 parent 9258f3b commit dea8dc0
Showing 1 changed file with 31 additions and 23 deletions.
54 changes: 31 additions & 23 deletions packages/common/http/src/fetch.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {inject, Injectable} from '@angular/core';
import {inject, Injectable, NgZone} from '@angular/core';
import {Observable, Observer} from 'rxjs';

import {HttpBackend} from './backend';
Expand Down Expand Up @@ -48,6 +48,7 @@ export class FetchBackend implements HttpBackend {
// We need to bind the native fetch to its context or it will throw an "illegal invocation"
private readonly fetchImpl =
inject(FetchFactory, {optional: true})?.fetch ?? fetch.bind(globalThis);
private readonly ngZone = inject(NgZone);

handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
return new Observable(observer => {
Expand Down Expand Up @@ -108,29 +109,36 @@ export class FetchBackend implements HttpBackend {
let decoder: TextDecoder;
let partialText: string|undefined;

while (true) {
const {done, value} = await reader.read();

if (done) {
break;
}

chunks.push(value);
receivedLength += value.length;

if (request.reportProgress) {
partialText = request.responseType === 'text' ?
(partialText ?? '') + (decoder ??= new TextDecoder).decode(value, {stream: true}) :
undefined;

observer.next({
type: HttpEventType.DownloadProgress,
total: contentLength ? +contentLength : undefined,
loaded: receivedLength,
partialText,
} as HttpDownloadProgressEvent);
const reqZone = Zone.current;

// Perform response processing outside of Angular zone to
// ensure no excessive change detection runs are executed
// Here calling the async ReadableStreamDefaultReader.read() is responsible for triggering CD
await this.ngZone.runOutsideAngular(async () => {
while (true) {
const {done, value} = await reader.read();

if (done) {
break;
}

chunks.push(value);
receivedLength += value.length;

if (request.reportProgress) {
partialText = request.responseType === 'text' ?
(partialText ?? '') + (decoder ??= new TextDecoder).decode(value, {stream: true}) :
undefined;

reqZone.run(() => observer.next({
type: HttpEventType.DownloadProgress,
total: contentLength ? +contentLength : undefined,
loaded: receivedLength,
partialText,
} as HttpDownloadProgressEvent));
}
}
}
});

// Combine all chunks.
const chunksAll = this.concatChunks(chunks, receivedLength);
Expand Down

0 comments on commit dea8dc0

Please sign in to comment.