Angular async lazy rendering with RxJS
Asked Answered
S

6

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

Spoilage answered 3/7, 2020 at 19:1 Comment(9)
And what have you tried exactly?Caeoma
I think the correct approach here would be pagination or virtual scroll instead of dealing this with pure rxjsAnalogize
Check this: #37600654Boracic
depending on your app you might get a speed boost by using trackBy in your *ngFor. but real solutions are indeed pagination and virtual scrollDirectional
@AluanHaddad just provided a sample what I have tried so far @DPro I am using trackBy but yet not the performance are poorSpoilage
Given the two drawbacks to your solution using delay, would you consider it an acceptable solution if only those two issues were addressed?Caeoma
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.Aeneid
@AdrianBrand the item is pretty complicated, huge texts (by huge I mean 70pages of text per set), thats why the performance starts to degradedSpoilage
@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.Spoilage
S
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>
Sibell answered 9/7, 2020 at 21:10 Comment(1)
there is a startIndex Property 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 github.com/dhilt/ngx-ui-scroll/issues/194Sibell
S
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

Stewartstewed answered 7/7, 2020 at 4:17 Comment(0)
E
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
Eugenle answered 9/7, 2020 at 8:20 Comment(1)
The above works great but not for my case as my items has different sizes and programatically I need to scroll throw the list.Spoilage
T
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

Triennial answered 13/7, 2020 at 9:38 Comment(2)
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 answerSpoilage
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.Triennial
D
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

Dangerous answered 13/7, 2020 at 10:40 Comment(0)
S
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

Spoilage answered 17/7, 2020 at 4:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.