Huge amount of repaint/reflow (animationFrameRequest)

Case: View with nested use of custom elements
Seems that each dom element added causing animationFramerequest (RAF)

NB! not blaming framework :slight_smile: (I’m a beginner)
It’s a principal question:
Ho to reduce number of repaint / reflow in case where loading a view causing
big change in number of element in DOM

Problem is from an existing application in production state
Hopefully solvable without big rewrite
(can not use virtual repeat, row/cell hight not deterministic)

If you are running into perf issue, you can check if you are doing expensive work before or after a view is attached to the DOM. If possible, move logic related to manipulate elements from attached() to bind(), if you don’t have to calculate their real dimension. Would be easier to help if you can give some code examples. Beside that, some number would be nice: how big is the array, etc…

No manipulation in attached(), bind() used. Mayby it the css causing Aurelia putting
all elemets in requestAnimationFrame queue… ?

Modified aurelia-binding.js with a conso.log of queue.length
image

Loading real data, 91672 goes into queue.length (Aurelia queue request for animationFrame)
console when loading view in browser. get 117 message of
aurelia-binding.js:155 [Violation] ‘requestAnimationFrame’ handler took 77ms (variation 50ms - 150ms)

This is the connect-queue placing the observeProperty calls inside RAF. 91672 property descriptors (or a multiple thereof) are created to observe all your view models.
You can verify this by adding & oneTime to your interpolations and/or .one-time in your binding expressions.
What does your view look like?

Note: the problem isn’t really the RAF or connect-queue itself, but rather that there are a whopping 91k things to observe. This is going to be slow no matter what you do. If acceptable, using oneTIme bindings would be your way to go. That would work if the data properties don’t change after loading.

May I see how your view code looks like, and how you are assigning new array to the repeat?

If all observProperty goes into RAF, it’s a new
knowledge / understanding for me (how the RAF got into play)

Try to progress further with oneTime binding where it can bee done.

The view elements and repeets, in general a matrix:
nested structure of view and element’s
i.e. 500 person one week
ending in 500 x 7 -> 3500 operationalDuty

operationalPlan.html

<template view-cache="*">	
...
	<grid-row				
		repeat.for="employee of employees.items & oneWay"
		employee.one-time="employee"
		plan.one-time="$parent">
	</grid-row>
..
</template>

gridRow.html

<template view-cache="*">	
..
	<grid-cell-operational			    
		repeat.for="day of days & oneWay"
		employee.to-view="employee"
		day.to-view="day"
		plan.one-time="plan"
		group-view.one-time="groupView"
		containerless>
	</grid-cell-operational>
..
</template>

gridCellOperational.html

<template view-cache="*">
..
	<operational-duty
			repeat.for="duty of duties & oneWay & signal:'filter-updated':'duty-updated'"
			duty.to-view="duty"
			day.to-view="day"
			plan.one-time="plan"
			group-view.one-time="groupView"
			containerless>
	</operational-duty>
..
</template>

operationalDuty.html

<template view-cache="*">
...
	<!-- Information on a duty in the scheduler -->
...
</template>

Not all observeProperty goes into RAF.

How connect-queue works

The first 100 connect calls will execute immediately. Only when the number of bindings that need to be connected is higher than 100 will the items past the first 100 be queued with RAF.

Then there is also a limitation on how many are executed within a RAF. If the queue has been processing for 15ms, the remainder will go with the next RAF (this algorithm is a bit naive as the real budget of performing work is typically less than 7ms, this is a point for improvement).

Effectively what this does, is it ensures that your page will always load after no more than initialization time + 15ms + time to attach everything. It puts an upper bound on by how much the observation can slow down page loads.
It also means that for a very large number of bindings, your app will continue processing the initialized observations after the page appears to be done loading. This is likely what’s causing the several clogged-up RAF’s you see occurring after loading a view with lots of data in it.

To put it in other words: it’s the “price” you’re paying for the large number of items. The alternative would be to have to look at a completely not loaded page for the time that it currently takes for the RAFs to be done.

Ways to optimize

virtual-repeat
You already know about the virtual repeater. Technically this could be built upon to make it adapt to dynamic item height - this is something I’ll be experimenting with in vNext at least.

You might be able to “hack” around the non-deterministic height problem, as long as you know the minimum height of an element, to simply give it the lowest possible height.
It will render a bunch of off-screen items but still likely far fewer than the normal repeater does now.

when-visible
jdanyow has created a binding behavior for this sort of problem, which causes your bindings to only be updated if they’re visible on screen. I don’t know precisely how it works (still need to look into it myself) but in any case, it might be of use to you: https://github.com/jdanyow/aurelia-when-visible

use signals instead of observation
I’m not a fan of this idea because it kind of goes against the spirit of Aurelia. But if the other solutions aren’t viable and this is a critical thing to your app, you could just make everything one-time / & oneTime and use signals to update everything.

In vNext we’ll be addressing this with a more generalized switch for observation methods, in vCurrent it’s not possible to do this very cleanly unfortunately.

I also noticed you use & oneWay in your repeaters. This is redundant, just FYI. Collection bindings (or really any bindings that are not targeting form elements) are one-way / to-view (same thing) by default.

2 Likes

Some recraft of UX to get the virtual repeater to work
is kind of safe path, and should work. It is used several places in the application.

Thanks a lot, appreciate the comments !
Posting back later, solution selected to get around the issue

2 Likes

I have a similar setup and am running into the same problem. In my situation in the worst case scenario the connect queue has around 175 000 items which used to be around 250 000 but by combining string interpolation and removing unneeded bindables/features I removed quite a lot from the queue.

Unfortunately this doesn’t help enough. I have tried using the when-visible attribute, but it seems this does not have an impact on the size of the queue.
Ideally I want to use virtual-repeat because essentially my setup is a huge table which consists of multiple nested components. I have tried implementing virtual-repeat but there were quite a lot of issues with it not scrolling/rendering correctly so I cannot use it in production in it’s current state.

I was wondering if someone knows any other solutions/tricks I can try out to make my application load faster.

https://codesandbox.io/s/y36wv6vq1z Is this example remotely close to your scenario? it has around 1000 items, with each item contains anywhere between 1 -> 50 sub items. If not, can you update it so we can have some concrete example of what to optimize?

I’ve changed the number of items from 1000 to 3000, the length of the connect-queues are then about equal. Unfortunately even then the situation is not the same. In your example every cycle about 1000-4000 items get processed while in my applications this amount is about 400. It seems my items take way longer to process. This is probably because of the huge amount of if.bind’s, string interpolations, nested components and bindables I have.

This has made it very difficult to create a minimal example showcasing the problem. I’d really love to show you my exact problem but unfortunately I can’t put my sourcecode online.
I understand that it’s very difficult to locate the problem this way and me posting here is kind of a shot in the dark.

vscode liveshare could do

That would be amazing! When would you be available?

I can have a look tomorrow, mid night here. We can use gitter to chat easier :smiley:

Did you guys ever figure it out?
What techniques had the most impact (besides of course one-time binding)?

I’m absolutely certain that either virtual-repeat or when-visible (if they would actually work) would have by far the biggest impact. @dannyBies could you clarify (perhaps even create an issue in the repo) the issues you had with virtual-repeat? Even if it’s (too) tricky to solve in vCurrent, we certainly want this to be working flawlessly in vNext. It’s an important scenario and we’d like to get it right, without the need for hacks.

@khuongduybui due to the holiday we haven’t looked at this together.
What helped the most in my situation was to start using aurelia-store as this dramastically reduced the amount of bindings I needed. In one case this resulted in an average performance gain of 70%.

@fkleuver I agree with you that virtual-repeat would be an ideal solution, unfortunately the plugin is not in a production-ready state in my opinion. Most issues I ran into are already on the issue list (#83, #109 #138). I’ve tried most solutions/workaround but never got it to work properly.
I also tried making an example to showcase the problems I have with ui-virtualization but unfortunately I’m unable to replicate the problems because of the complex setup of my app.

Oh yes, using the central one source of truth can definitely reduce the amount of observers. I will be watching this topic to see if you guys come up with more creative solutions :slight_smile:

Just to keep you folks updated @dannyBies has taken the initiative to implement a suggestion regarding making the connect-queue size configurable, which seems to lead to significant performance improvements in some cases. https://github.com/aurelia/binding/pull/729

1 Like

Small update, I have disabled the connect queue in my application and this has resulted in performance increases of 20-60% in the initial loading of a screen with no downsides in my particular case.