Native Observables Reach Chrome 135
Chrome 135 ships with Native Observables, a Web API feature that delivers built-in support for observable streams with .when() convenience methods and automatic multicasting as the standard behavior. One important caveat: these observables are not identical to RxJS Observables in terms of design and semantics. Differences include Promise-returning methods and the reliance on AbortController for handling cancellation.
Native Observables in Chrome
Chrome 135 now ships with Native Observables — though these shouldn't be mistaken for the RxJS Observables we work with daily. Before comparing the two, it's worth seeing how deeply this feature is woven into existing browser APIs.
Up to now, attaching callbacks to DOM elements meant reaching for addEventListener(). With the arrival of native Observables, those same elements expose a when() method that hands back an Observable — a clear sign of how native this implementation really is.
// until now
document.addEventListener('mousemove', console.log);
// with native Observables
document.when('mousemove').subscribe(console.log);
How They Differ from RxJS Observables
1. Multicasting Is the Default
Native Observables come preconfigured to be shared. This means they multicast by default, only start executing once the first subscriber arrives, and don't replay anything that was emitted earlier. It behaves much like applying share() in RxJS — but crucially, not shareReplay().
Consider this RxJS example:
const numbers$ = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
});
});
numbers$.subscribe((n) => console.log(`Sub 1: ${n}`));
numbers$.subscribe((n) => console.log(`Sub 2: ${n}`));
The output looks like this:
Sub 1: 1
Sub 1: 2
Sub 2: 1
Sub 2: 2
Sub 1: 3
Sub 2: 3
However, the identical snippet written against native Observables yields a different result:
Sub 1: 1
Sub 1: 2
Sub 1: 3
Sub 2: 3
The explanation: the initial subscription kicks off execution inside the Observable, and numbers 1 and 2 get delivered synchronously. Only value 3 arrives within an asynchronous task, which is why the second subscriber catches it too.
2. Operators Are Method Calls
Rather than reaching for pipe() as we do in modern RxJS, native Observables return to an older style: operators such as map() and filter() exist as methods directly on the Observable object.
In RxJS, we'd write:
// RxJS Observables
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(2);
subscriber.next(4);
subscriber.next(8);
});
numbers$
.pipe(
map((n) => n * 2),
tap((n) => console.log(`tap: ${n}`)),
filter((n) => n < 10),
)
.subscribe(console.log);
With native Observables, the same logic reads differently:
// Native Observables
const numbers$ = new Observable((subscriber) => {
subscriber.next(2);
subscriber.next(4);
subscriber.next(8);
});
numbers$
.map((n) => n * 2)
.inspect((n) => console.log(`tap: ${n}`)) // <-- that's tap()
.filter((n) => n < 10)
.subscribe(console.log);
3. Methods That Hand Back Promises
Certain operators — first(), last(), reduce(), and forEach() among them — produce a Promise as their result rather than another Observable. This also removes the need to call subscribe explicitly.
That design makes them far more comfortable to drop into async/await code.
RxJS provides utility helpers to turn an Observable into a Promise:
// RxJS
const countdown = new Observable((subscriber) => {
let counter = 1;
const intervalId = setInterval(() => {
subscriber.next(counter++);
if (counter > 5) {
clearInterval(intervalId);
subscriber.complete();
}
});
});
await lastValueFrom(countdown.pipe(tap(console.log)));
console.log('ended');
Native Observables, for their part, let last() return a Promise directly:
// Native Observables
const countdown = new Observable((subscriber) => {
let counter = 1;
const intervalId = setInterval(() => {
subscriber.next(counter++);
if (counter > 5) {
clearInterval(intervalId);
subscriber.complete();
}
});
});
await countdown.inspect(console.log).last();
console.log("ended");
4. AbortController Replaces unsubscribe()
Ending a subscription is achieved through AbortController, the same mechanism we use to cancel fetch() calls. Since Angular's resource() function also adopts this pattern, it should look very familiar already.
RxJS code normally relies on calling unsubscribe when no further values are desired:
// RxJS
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
});
const subscription = numbers$.subscribe((value) => {
console.log(value);
if (value >= 1) {
console.log('aborting/unsubscribing (even synchronously)');
}
});
subscription.unsubscribe();
Because native Observables fit more tightly into the Web API family, cancellation goes through AbortController:
// Native Observables
const numbers$ = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
});
const abortController = new AbortController();
numbers$.subscribe(
(value) => {
console.log(value);
if (value >= 1) {
console.log("aborting/unsubscribing (even synchronously)");
abortController.abort(); // <-- "unsubscribe" here
}
},
{ signal: abortController.signal },
);
5. Cleanup with addTeardown()
The Observable constructor no longer receives its teardown function as a return value. Instead, any cleanup logic is registered directly via addTeardown().
RxJS's approach looks like this:
// RxJS Observable
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
return () => console.log('complete inside the observable');
});
With native Observables, teardown gets attached in a more explicit way:
// Native Observables
const numbers$ = new Observable((subscriber) => {
subscriber.addTeardown(() => {
console.log("completes inside the observable");
subscriber.complete();
});
subscriber.next(1);
subscriber.next(2);
});
Will Observables Climb to Language Level?
Whether Observable ends up as a core language-standard construct — analogous to Promise — or remains at the browser-level Web API tier is still up in the air. That choice will determine how broadly they end up supported across different environments.
https://github.com/wicg/observable?tab=readme-ov-file#standards-venue
What Happens to RxJS?
Development on RxJS 8 had been on hold until native Observables were ready. Now that they're out, RxJS plans to move forward with incorporating them — and to supply shims for environments that don't yet support the feature.
https://github.com/ReactiveX/rxjs/issues/6367
Any Impact on Angular Signals?
Nothing shifts there. Signals are meant for state, while Observables handle events — whether those are DOM events or async notification streams. Observables still carry value for modeling event triggers, yet Angular's direction for state management points toward Signals, not back to BehaviorSubject.
If you'd like to try the examples for yourself, a ready-to-run starter project is available at https://stackblitz.com/edit/stackblitz-starters-ybgwzhbc
Framework Interactions
Sarah Drasner delivered a presentation at dotJS examining how contemporary frontend frameworks have continually shaped each other's evolution.
She highlighted, for example, that Qwik’s resumability concept appears to have drawn from Wiz, Google’s in-house framework that is currently being merged into Angular.
Unsurprisingly, she devoted considerable attention to Angular's present position within this dynamic ecosystem. Through capabilities such as incremental hydration, Angular is now poised to shape other frameworks in response.
To wrap up her session, she introduced an emerging open-source project called tsurge, which she proposed could serve as a complement to Angular’s existing ng update tooling.
NgRx 19.1
Version 19.1 of NgRx has been made available, introducing several fresh additions to the SignalStore across these areas:
- Testing
- Custom Features
withEntities
https://github.com/ngrx/platform/blob/main/CHANGELOG.md#1910-2025-04-01

