DEV Community

Cover image for Angular: Async Rendering with a single Rx Operator
Giancarlo Buomprisco for Angular

Posted on • Originally published at angularbites.com

Angular: Async Rendering with a single Rx Operator

This post was originally published on Angular Bites

The concept of async rendering, in the way I mean it, is simple: the process of rendering items on screen is scattered so that the browser won't block until all items have been rendered.

So here's how it works: I render item one, then I wait a little bit, then render the next item, and so on. In between, the browser can execute all the other scheduled events in the loop before we let it render again.

When and Why you should use it, sometimes

When does this work (particularly) well?

  • In case we are rendering particularly long and heavy lists
  • In case each item of the list takes a lot of space on the page

Why? Your app will "look" faster. It's not going to be actually faster, but your users will perceive it as being so. Good enough.

A single-operator approach

In the past I've solved this in various ways, as I described in How to Render Large Lists in Angular.

This time I thought of a single operator that would sequentially scatter the rendering process of a subset of the array.

We'll call this operator lazyArray. It supports two arguments:

  • delayMs = how long the browser should wait before it renders the next array
  • concurrency = how many items to render at once

Just show me the code, Giancarlo!

Alright, here it is:

export function lazyArray<T>(
  delayMs = 0,
  concurrency = 2
) {
  let isFirstEmission = true;

  return (source$: Observable<T[]>) => {
    return source$.pipe(
      mergeMap((items) => {
        if (!isFirstEmission) {
          return of(items);
        }

        const items$ = from(items);

        return items$.pipe(
          bufferCount(concurrency),
          concatMap((value, index) => {
            const delayed = delay(index * delayMs);

            return scheduled(of(value), animationFrameScheduler).pipe(delayed);
          }),
          scan((acc: T[], steps: T[]) => {
            return [ ...acc, ...steps ];
          }, []),
          tap((scannedItems: T[]) => {
            const scanDidComplete = scannedItems.length === items.length;

            if (scanDidComplete) {
              isFirstEmission = false;
            }
          }),
        );
      }),
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

Usage

Using it is pretty simple, use it just like any other operator:

@Component({ ... })
export class MyComponent {
   items$ = this.service.items$.pipe(
     lazyArray()
   );
}
Enter fullscreen mode Exit fullscreen mode

Let's break it down, shall we?

We want to keep track whether it's the first emission, or not. We only want to render lazily the first time:

let isFirstEmission = true;
Enter fullscreen mode Exit fullscreen mode

We transform the array into a stream of items:

const items$ = from(items);
Enter fullscreen mode Exit fullscreen mode

We collect the amount of items into an array based on the concurrency:

bufferCount(concurrency),
Enter fullscreen mode Exit fullscreen mode

We scheduled the rendering based on the delay, and then progressively increase the delay based on the item's index:

concatMap((value, index) => {
  const delayed = delay(index * delayMs);

  return scheduled(of(value), animationFrameScheduler).pipe(delayed);
})
Enter fullscreen mode Exit fullscreen mode

We keep collecting the processed items into a single array:

scan((acc: T[], steps: T[]) => {
  return [ ...acc, ...steps ];
}, [])
Enter fullscreen mode Exit fullscreen mode

Finally, we check if the amount of processed items is as long as the initial list. In this way, we can understand if the first emission is complete, and in case we set the flag to false:

tap((scannedItems: T[]) => {
  const scanDidComplete = scannedItems.length === items.length;

  if (scanDidComplete) {
    isFirstEmission = false;
  }
})
Enter fullscreen mode Exit fullscreen mode

Demo

I came up with this because my application, Formtoro, loads quite a bit of data at startup that renders lots of Stencil components at once.

It did not work well, it was laggy. I didn't like it, so I found a way to solve it. I'll show you the differences:

Without lazyArray operator:

Without Lazy Array

With lazyArray operator:

With Lazy Array


This approach works very well in my case - and may not in yours. Shoot me an email if you want help implementing it.

Ciao!


If you enjoyed this article, follow me on Twitter or check out my new blog Angular Bites

Top comments (4)

Collapse
 
samvloeberghs profile image
Sam Vloeberghs

Very interesting! Thanks for sharing!

Collapse
 
stradivario profile image
Kristiqn Tachev • Edited

Awesome operator!

I manage to make it simpler

export const lazyArray = <T>(
  delayMs = 0,
  concurrency = 2,
  isFirstEmission = true
) => (source$: Observable<T[]>) =>
  source$.pipe(
    mergeMap(items =>
      !isFirstEmission
        ? of(items)
        : from(items).pipe(
            bufferCount(concurrency),
            concatMap((value, index) =>
              scheduled(of(value), animationFrameScheduler).pipe(
                delay(index * delayMs)
              )
            ),
            scan((acc: T[], steps: T[]) => [...acc, ...steps], []),
            tap((scannedItems: T[]) =>
              scannedItems.length === items.length
                ? (isFirstEmission = false)
                : null
            )
          )
    )
  );

Enter fullscreen mode Exit fullscreen mode
Collapse
 
rakiabensassi profile image
Rakia Ben Sassi • Edited

Thanks for sharing this piece Giancarlo!
I'm faced with the following compile error (I'm using TypeScript 4.8.4 and rxjs 7.5.7):
error TS2345: Argument of type 'OperatorFunction' is not assignable to parameter of type 'OperatorFunction'

Does anybody faced the same problem or know how to fix it?

Since you've published this article in 2020, I would like to know if this solution is still applicable with Angular 15 (where I'm using rxjs 7.5.7) or if there are better ways?

Collapse
 
gc_psk profile image
Giancarlo Buomprisco

Hi @rakiabensassi! I think I'd look into rxAngular these days, it will probably do a better job. In some cases, though, I think the above can still help you.