Vaadin-grid: RecyclingComponentRenderer

Created on 6 Sep 2019  路  23Comments  路  Source: vaadin/vaadin-grid

Currently the ComponentRenderer creates new component per every Grid cell. Upon scrolling, the new components state need to be transmitted to the client-side and a web component needs to be constructed. That leads to a horrible performance - 800kb JSONs are sent every 20ms to the client, the CPU skyrockets and the FPS drops to 3.

The use-case is for example a list of groceries in a grocery shop, every grocery item being one row in a Grid with one column (the Grid is only used for the purpose of scrolling and lazy-loading of a long list; say Beverage Buddy list of beverages as demoed here: https://beverage-buddy-vok.herokuapp.com/ - that's a Grid with one column. I think that demo runs Vaadin 13 and it runs quite good, however the performance of Vaadin 14 Grid is way worse). A server-side GroceryLayout is constructed, with 30 components nested inside.

The workaround is to use TemplateRenderer obviously. However, I'd like to open a discussion whether we wish to support a fast-performing ComponentRenderer, or whether we would "prohibit" such use-case (and document that it's going to perform poorly), or fix this as a performance regression.

enhancement flow

Most helpful comment

@mstahv Yes. @heruan encountered a situation with ComponentRenderers where the Grid basically gives up and shows nothing. The Grid then starts exhibiting random visual failures (cells not populated, shadow scroll bar appearing when scrolling, browser freezes, huge JSONs sent from the server-side). It can be safely assumed that the situation is infinitely worse. The current implementation renders ComponentRenderers useless, except perhaps for most basic use-cases.

All 23 comments

The idea behind the RecyclingComponentRenderer is as follows: For example in Android, the ListView only creates components for visible items. When the user scrolls down, the components "fall out" at the top. However, the components are not thrown away but added to the bottom and only updated with new data.

Alternatively we could create a specialized component for this purpose - a one-column grid-like component?

How would RecyclingComponentRenderer be any different from using ComponentRenderer with the constructor where you provide a componentUpdateFunction callback which is supposed to update an existing component instance rather than create a new one?

karibu10-helloworld-application.zip

Testing app that reproduces the issue. Just run ./gradlew appRun and browse http://localhost:8080 . A simple FlexLayout with 30 spans is created. Please observe how the scrolling is slow, the networking shows UIDLs of 1mb JSONs there.

@Legioth the componentUpdateFunction is only triggered on DataProvider.refreshItem() call; it's not called to 'recycle' a component that fell out of the Grid. To verify this assumption I've placed a breakpoint into the testing project - the componentUpdateFunction closure never gets called as you scroll the Grid.

My idea was to create a new RecyclingComponentRenderer that would explicitly call componentUpdateFunction also to recycle, to keep backward compatibility, but I'm also fine with modifying existing ComponentRenderer+Grid machinery to call componentUpdateFunction also to recycle.

The example app actually provides componentUpdateFunction but has the same performance as when a new component is constructed for every item.

Any update on this? Would be good to have some form of smooth and fast loading grid with componentRenderer.

@mvysny Do you have some numbers that how much worse the situation is with V14? All regressions, including performance regressions should be eagerly fixed.

Confirming same behaviour. Please can you fix it somehow asap.

Are there are any plan to fix this slow rendering?

Look situations where Grid constructed with ComponentRenderers, for example with 40 rows per 40 columns. It is 1600 parameters to change in the Grid if full update is needed. It will generate a lot of JSON traffic. But what if i change only one component's value? What if i change 10 or 100 values?
I create some class based on Javers lib, that compare current Grid's data with the new loaded from database. It detect fields of items that was changed and send update command to exactly that components created for that fields. If more than one field is updated in item - i call refreshItem() on DataProvider. It work good, save amount of traffic and adds responsibility to the UI. Until i need to ADD or REMOVE some items from dataset. It can process only over refreshAll() method, as i know. And this generate a lot of traffic again.
Conclusion: if we have a method like refreshCell() that can update only one component, or have an ability to add/remove rows without full reconstruct Grid in browser it may give fast and stable Grid to us.
P.S. There is also some column sort problems with my compare method, when click on column header. I resolve it by comparing last sorted column with changed fields. If some field changed that somehow bind to last sorted column - there is need to refreshAll() calling.

@mstahv Yes. @heruan encountered a situation with ComponentRenderers where the Grid basically gives up and shows nothing. The Grid then starts exhibiting random visual failures (cells not populated, shadow scroll bar appearing when scrolling, browser freezes, huge JSONs sent from the server-side). It can be safely assumed that the situation is infinitely worse. The current implementation renders ComponentRenderers useless, except perhaps for most basic use-cases.

We agree with Martin 100% and still waiting for enhancement

If just getting the initial set of items to render is already way too slow, then the idea of recycling items while scrolling is probably too little and too late?

There's of course less sensitivity for latency when scrolling compared to when initially loading a view, but it the difference even meaningful?

I have had to limit the number of rows within my component grid as the first initial rendering of the tiles I had was too slow. This would break when I calculate sizes as not everything would be rendered, clicking on the tile would adjust the size correctly. I had to put a small delay on the tile to apply the sizes once the grid had been rendered making it even slower.

It sounds like recycling a component would fix this issue, i'd rather the user be able to scroll through my whole grid before reaching a limit I had to implement.

I've crated a really rough prototype for a recycling renderer. I'd appreciate if you could try it out with your problematic use cases to see how it impacts performance with a real-world use case.

The main challenge with a recycling server-side renderer is that the server doesn't how the client will end up reusing elements which means that the server cannot know which existing component instance to use for a specific requested item. The prototype deals with this by not updating the component when new items are fetched, but instead renders a placeholder in each cell and then lets that placeholder separately request updates for its contents when the client-side grid assigns a specific item to it.

The drawback of this approach is that an additional round trip is required since the component updates are requested only after the regular data response has been processed. There is a possibility that there is some mechanism in the depths of the client-side grid that would allow knowing how the physical elements would be reused at the time when new items are fetched. If that it the case, then I assume @tomivirkki will be able to tell me so that I could adjust the prototype to avoid that additional round trip.

This additional round trip also leads to cases when old content may be visible in cells after scrolling. This gets more visible the longer the network latency to the server is. It would be possible to manually set the cell content as visible until it has been confirmed that the content is updated, but I didn't bother to implement that in the prototype because there's still a chance that everything could be handled in only a single round trip.

The prototype is currently implemented directly inside my very simple test application rather than as a standalone add-on. To test it, you'd thus need to copy the Java and JavaScript to your own project. You also need to add @JsModule("./recyclingRenderer.js") to some suitable part of the application, e.g. the main layout or the view that would use the recycling renderer.

There are two different factory methods for the renderer. The simpler, withoutReset takes one callback for creating a new component and one callback for updating an existing component of unknown state to show a specific item. The more complex factory, withReset returns a Registration from the update callback. This registration will be run to reset the component to its baseline state before the component is reused. In this way, the update callback itself can potentially be simpler because it doesn't have to take into account multiple different states of the component to reuse.

The prototype is in https://github.com/Legioth/recycling-renderer

I'm posting a project which reproduces the issue. The initial page loads quickly and even scrolls very nicely if you only use mouse scroll wheel. However, once you start dragging the grid scroll bar, the performance deteriorates rapidly. I believe the performance can be degraded even further by using more complicated component hierarchies (or more complex components with more state); at that time the delays in Grid's response to scrolling will grow so big that the Grid is perceived by the user as malfunctioning.

Since the initial Grid rendering including all of its cells works quite quickly, I believe the recycling renderer idea is applicable and should improve the performance.

The example project:
skeleton-starter-flow.zip

The video:
Screencast from 15.06.2020 10.27.17.webm.zip

I'll try to apply @Legioth prototype - just a second.

I've tried Legioth patches and I'm happy to say that the performance improved rapidly. I'm attaching a patched Legioth project, and a video.

recycling-renderer.zip
Screencast from 15.06.2020 11.12.10.webm.zip

The RecyclerRenderer works really nice with slower network speeds as well. Without the RecyclerRenderer, the Grid shows the double scroll bar (as shown in the first video) and just looks like it "doesn't really want to scroll where you want it". It feels ... broken, unresponsive, like I have to fight with the component to scroll where i want (in extreme cases with network lags and/or more complex hierarchy). With RecyclerRenderer the Grid feels snappy - it nicely follows the scrolling bar. Yes it still takes ages to render those cells, but at least the Grid listens to my "scrolling commands" really responsively.

Regarding the withReset/withoutReset: Android simply gives you the recycled instance and you as the programmer are responsible to modify its entire state. If you forget to change the background and items are colored differently, it's the problem of the developer. Therefore I'd personally vote for the "withoutReset" - we can simply hide the components until they have been updated and are ready to be shown.

@heruan I remember you have been bitten by the poor Grid performance. Could you please try out the approach created by Legioth, whether it improves the performance in your project as well?

It seems like the erratic behaviour when dragging the scrollbar is a bug of its own related to also requesting data for intermediate rows that are very quickly scrolled past. I've created vaadin/vaadin-grid-flow#1040 to cover that issue.

@heruan I remember you have been bitten by the poor Grid performance. Could you please try out the approach created by Legioth, whether it improves the performance in your project as well?

I've tried @Legioth RecyclingRenderer, performance is better; a couple of issues:

  1. cells with the renderer appear with the placeholder string at first, so I've replaced that with an empty string but still the grid appears not fully rendered for a second;
  2. when navigating away from the page and then going back, the renderer doesn't work anymore and the cells appear just blank.

P.S. You can see the behavior on the testing app which you have access to.

That placeholder text was indeed not intended to remain there. Sorry about that.

Seeing an empty grid for a second or so is a side effect of the additional round trip that is required in this case. This could actually be "prerendered" based on some quite safe assumptions when the grid is initially displayed, but incremental loading might still have to use the additional round trip.

The problem with navigating back is most likely not a fundamental solution, but only a case that I didn't handle properly in my quick prototype.

The problem with navigating back is most likely not a fundamental solution, but only a case that I didn't handle properly in my quick prototype.

Of course @Legioth I understand, I was just giving feedback. Thanks for the quick prototype! Tell me if I can help you debug the navigating back issue.

Other issues:

  1. When sorting by a grid column, is it possible that the rendered values are mixed up on the wrong cells? I've just tested, this happens only with the recycling renderer (I'll double-check though);
  2. I'm getting this exception (never seen before, not sure if related):
java.lang.IllegalStateException: Connector can only be initialized for an attached Grid
    at com.vaadin.flow.component.grid.Grid.lambda$initConnector$4(Grid.java:1354)
    at java.util.Optional.orElseThrow(Optional.java:408)
    at com.vaadin.flow.component.grid.Grid.initConnector(Grid.java:1354)
    at com.vaadin.flow.component.grid.Grid$GridArrayUpdaterImpl.initialize(Grid.java:1104)
    at com.vaadin.flow.data.provider.DataCommunicator.lambda$requestFlush$2f364bb9$1(DataCommunicator.java:423)
    at com.vaadin.flow.internal.StateTree.lambda$runExecutionsBeforeClientResponse$1(StateTree.java:368)
    at java.util.ArrayList.forEach(ArrayList.java:1540)
    at com.vaadin.flow.internal.StateTree.runExecutionsBeforeClientResponse(StateTree.java:365)
    at com.vaadin.flow.server.communication.UidlWriter.encodeChanges(UidlWriter.java:411)
    at com.vaadin.flow.server.communication.UidlWriter.createUidl(UidlWriter.java:187)
    at com.vaadin.flow.server.communication.UidlRequestHandler.writeUidl(UidlRequestHandler.java:121)
    at com.vaadin.flow.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:90)
    at com.vaadin.flow.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:40)
    at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1540)
    at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:247)
    at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:120)
Was this page helpful?
0 / 5 - 0 ratings