5

I am looking for away to do "lazy rendering" with RxJS in Angular, what I want to achieve is the following:

<div *ngFor="let item of items$ | async">
  {{item.text}}
<div>

and in the component I have:

export class ItemsComponent implements OnInit {
  public items$: Observable<Item[]>;
  
  constructor(private setStore: SetStore){}

  ngOnInit() {
     const setId = 1;
     this.items$ = this.setStore.sets$.pipe(map(sets => sets.find(set => set.id = 1).items));
  }
}

And this works fine but when the set has +50 items, the rendering takes time and it freeze's for a second or more. I was looking for a way to do it lazy by somehow rendering first 30 items and then do load the next 30 after 500ms and so on until the list reach's its end.

Edit: I have tried this approach:


const _items$ = this.setStore.sets$.pipe(
  map(sets => sets.find(set => set.id == 1).items)
);
const loadedItems = [];
_items$.subscribe(data => {
  this.items$ = from(data).pipe(
    concatMap(item => {
        loadedItems.push(item);
        return of(loadedItems).pipe(delay(1));
      })
    );
  });
})

The above works fine in terms of lazy rendering but has some disadvantages like:

  • initially you don't have see any item in the page
  • items are loaded one by one every 1ms, not in batch

The above codes are not tested, if needed I can provide a sample

Sabri Aziri
  • 4,084
  • 5
  • 30
  • 45
  • 1
    And what have you tried exactly? – Aluan Haddad Jul 04 '20 at 02:41
  • 4
    I think the correct approach here would be [pagination](https://en.wikipedia.org/wiki/Pagination) or [virtual scroll](https://material.angular.io/cdk/scrolling/overview) instead of dealing this with pure rxjs – Tal Ohana Jul 04 '20 at 06:34
  • Check this: https://stackoverflow.com/questions/37600154/how-can-i-speed-up-ngfor-for-a-large-array – Jonathan Stellwag Jul 04 '20 at 06:57
  • 2
    depending on your app you might get a speed boost by using `trackBy` in your `*ngFor`. but real solutions are indeed pagination and virtual scroll – D Pro Jul 04 '20 at 09:32
  • 1
    @AluanHaddad just provided a sample what I have tried so far @DPro I am using `trackBy` but yet not the performance are poor – Sabri Aziri Jul 04 '20 at 18:08
  • 1
    Given the two drawbacks to your solution using delay, would you consider it an acceptable solution if only those two issues were addressed? – Aluan Haddad Jul 04 '20 at 19:10
  • You really shouldn't have issues rendering a component 50 times, I have complicated tables that render thousands of rows before performance starts to degrade. – Adrian Brand Jul 07 '20 at 04:43
  • @AdrianBrand the item is pretty complicated, huge texts (by huge I mean 70pages of text per set), thats why the performance starts to degraded – Sabri Aziri Jul 10 '20 at 14:10
  • @AluanHaddad if the initial load(e.x 30 items) and then the partially load is done throw an interval(e.x every 50ms load next 30items until the end) yes I would accept it as the right solution. – Sabri Aziri Jul 10 '20 at 14:11

6 Answers6

4

You can use Virtual Scrolling with Different items sizes using ngx-ui-scroll

demo with variable height items it is quite simple to start with

<div class="viewport">
  <div *uiScroll="let item of datasource">
    <b>{{item.text}}</b>
  </div>
</div>
hanan
  • 1,768
  • 1
  • 14
  • 19
  • 1
    there is a startIndex Property https://dhilt.github.io/ngx-ui-scroll/#/settings#start-index But I think u are looking for specific function like scrollToIndex or ScrollTo(predicate) I have created a issue for it https://github.com/dhilt/ngx-ui-scroll/issues/194 – hanan Jul 10 '20 at 19:35
2

From what I understand all you are missing is an additional buffer operator

And regarding the first bullet (initial items), you can skip the first 30 delays

Nitsan Avni
  • 753
  • 6
  • 6
1

If the rendering is what is taking so long, it sounds like the component UI is complex enough to affect rendering performance -- unlike a simple table. In such a case, you need to limit rendering (typically by using pagination or virtual scrolling).

Using Angular, your best bet is CDK Virtual Scroll: https://v9.material.angular.io/cdk/scrolling/overview (v9)

It's a very simple replacement of *ngFor, but the performance gains are instantly notable.

Example:

<div>
  <div *ngFor="let item of items" class="example-item">{{item}}</div>
</div>

becomes:

<cdk-virtual-scroll-viewport itemSize="100" class="example-viewport">
  <div *cdkVirtualFor="let item of items" class="example-item">{{item}}</div>
</cdk-virtual-scroll-viewport>

NOTE:

  • itemSize here is the fixed pixel-height of the component
  • look into templateCacheSize, trackBy for further performance considerations
kflo411
  • 94
  • 5
  • The above works great but not for my case as my items has different sizes and programatically I need to scroll throw the list. – Sabri Aziri Jul 09 '20 at 12:52
1

I'd change your code a bit and remove |async implementation. To process response in bulk or batch, i'd rather prefer to use bufferCount operator as shown below,

NOTE: This is dummy code but this is what you can use to make your life easy. In my example, I'm getting an array as result, in your case it could be an object or array of objects (I don't know);

items = [];


  constructor(public httpClient: HttpClient) {
    range (1, 30)                                        // total 30 requests
      .pipe(
        tap(x => {
          if(x<=10){
            this.items.push(x)                           // processing 10 requests by default without delay
          }
        })
      ).pipe(
          skip(10),                                     // skipping already processed 10 requests
          bufferCount(10),                              // processing 10 request in batch
          concatMap(x => of(x).pipe(delay(3000))) 
      ).subscribe(result=>{
         console.log(result);
         this.items = [...this.items, ...result]; 
       })
  }   

.html

<div *ngFor="let item of items">
  {{item}}
</div>

Dummy Demo

micronyks
  • 54,797
  • 15
  • 112
  • 146
  • Cool solution but I need to use async and be able to have load initially "30" items without delay, if you address this 2 I can accept it as right answer – Sabri Aziri Jul 13 '20 at 10:48
  • answer updated !.... `async ` implementation doesn't make sense here as you are processing data in a batch. So when a batch request is resolved, it's gonna emit a new data stream which will overwrite previously emitted data steam. So you will lose data emitted earlier. – micronyks Jul 13 '20 at 11:56
0

I would also agree that implementing virtual scrolling would be a solution because if you have 1000 rows, the rendering would starting to becoming slow anyway, but another way to reduce the cost of the rendering is to provide a trackBy function to your @ngfor loop.

Improve-performance-with-trackby

StPaulis
  • 2,844
  • 1
  • 14
  • 24
0

I found this to work well for me:

export class AppComponent implements OnInit {
  
  items$: Observable<number[]>;
  
  constructor() {}

  ngOnInit(){
    const source$ = from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    
    const items = [];
    this.items$ = source$.pipe(
          bufferCount(3),
          concatMap((items, index) => of(items).pipe(delay(index == 0 ? 0 : 3000))),
          map(arr => {
            items.push(...arr);
            return [...items];
          })
        );
  }
}

Demo

Sabri Aziri
  • 4,084
  • 5
  • 30
  • 45