Skip to content

Commit c5608e5

Browse files
JeanMechethePunderWoman
authored andcommitted
fix(http): Run fetch request out the angular zone (angular#50981)
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 angular#50979. PR Close angular#50981
1 parent 57ea887 commit c5608e5

File tree

1 file changed

+31
-23
lines changed

1 file changed

+31
-23
lines changed

packages/common/http/src/fetch.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {inject, Injectable} from '@angular/core';
9+
import {inject, Injectable, NgZone} from '@angular/core';
1010
import {Observable, Observer} from 'rxjs';
1111

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

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

111-
while (true) {
112-
const {done, value} = await reader.read();
113-
114-
if (done) {
115-
break;
116-
}
117-
118-
chunks.push(value);
119-
receivedLength += value.length;
120-
121-
if (request.reportProgress) {
122-
partialText = request.responseType === 'text' ?
123-
(partialText ?? '') + (decoder ??= new TextDecoder).decode(value, {stream: true}) :
124-
undefined;
125-
126-
observer.next({
127-
type: HttpEventType.DownloadProgress,
128-
total: contentLength ? +contentLength : undefined,
129-
loaded: receivedLength,
130-
partialText,
131-
} as HttpDownloadProgressEvent);
112+
const reqZone = Zone.current;
113+
114+
// Perform response processing outside of Angular zone to
115+
// ensure no excessive change detection runs are executed
116+
// Here calling the async ReadableStreamDefaultReader.read() is responsible for triggering CD
117+
await this.ngZone.runOutsideAngular(async () => {
118+
while (true) {
119+
const {done, value} = await reader.read();
120+
121+
if (done) {
122+
break;
123+
}
124+
125+
chunks.push(value);
126+
receivedLength += value.length;
127+
128+
if (request.reportProgress) {
129+
partialText = request.responseType === 'text' ?
130+
(partialText ?? '') + (decoder ??= new TextDecoder).decode(value, {stream: true}) :
131+
undefined;
132+
133+
reqZone.run(() => observer.next({
134+
type: HttpEventType.DownloadProgress,
135+
total: contentLength ? +contentLength : undefined,
136+
loaded: receivedLength,
137+
partialText,
138+
} as HttpDownloadProgressEvent));
139+
}
132140
}
133-
}
141+
});
134142

135143
// Combine all chunks.
136144
const chunksAll = this.concatChunks(chunks, receivedLength);

0 commit comments

Comments
 (0)