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);
Enter fullscreen mode Exit fullscreen mode

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}`));
Enter fullscreen mode Exit fullscreen mode

The output looks like this:

Sub 1: 1
Sub 1: 2
Sub 2: 1
Sub 2: 2
Sub 1: 3
Sub 2: 3
Enter fullscreen mode Exit fullscreen mode

However, the identical snippet written against native Observables yields a different result:

Sub 1: 1
Sub 1: 2
Sub 1: 3
Sub 2: 3
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 },
);

Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

Relationship between Signals and Observables


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