-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Improve "Debounce your scroll handlers" and establish better best practice for "Avoid layout thrashing" #2227
Comments
@paullewis - can you please take a look and adjust as necessary. thanks! |
Ok, let's go through this bit by bit: Debouncing The general idea of the code I wrote was to suggest that you want to debounce all handlers in this way, not just scroll. If you were to add listeners for two events, say a document.addEventListener('touchend', function () {
requestAnimationFrame(update);
});
document.addEventListener('click', function () {
requestAnimationFrame(update);
});
function update(now) {
console.log(now);
} You will get two rAF callbacks firing immediately, one after the other. The generalized solution here would be: var scheduledAnimationFrame = false;
var lastTouchInfo = null;
var lastClickInfo = null;
document.addEventListener('touchend', function (evt) {
if (scheduledAnimationFrame) return;
scheduledAnimationFrame = true;
lastTouchInfo = evt.touches;
requestAnimationFrame(update);
});
document.addEventListener('click', function (evt) {
if (scheduledAnimationFrame) return;
scheduledAnimationFrame = true;
lastClickInfo = {x: evt.clientX, y: evt.clientY};
requestAnimationFrame(update);
}); This will now mean you get one animation frame callback fired, in which you can now handle. Your rAF callback would be something like: function update () {
// Process the input: scrolls, touchmove, click, etc.
handleInputFromFrame();
// Do any visual work.
updateElementsAndOtherAnimations();
} So you're right that we don't need to debounce for a single handler, but by doing all handlers this way we are protected if we have multiple events that fire in a single frame. Next, your proposed solution of running the callback in a When to read, when to write You're suggesting that we should write only inside of a If you have multiple components you do need to do something similar to FastDOM, where each component separately registers its need to read, and the mutate the DOM. Your rAF then becomes: function update () {
// Process the input: scrolls, touchmove, click, etc. if needed
handleInputFromFrame();
// Process all reading... If needed.
readers.forEach(function(reader) {
reader.read();
});
// Then all writing... If needed.
writers.forEach(function(writer) {
writer.write();
});
// Do any visual work. If needed.
updateElementsAndOtherAnimations();
} Other miscellaneous bits
and ...
I'm not sure anyone is suggesting it's faster, just more in-line with the browser's frame lifecycle. In fact you can do reads inside of an input event handler, and the values would be available from the previous frame's layout pass in the same way as with rAF. However, if your event handlers runs long for some reason and you have a rAF then it will run late, i.e. not close to vsync, and generally it's the case we want rAF to run as close to vsync as possible.
That's only sort-of true in Blink's case. After the previous frame commits, and the next frame starts, Blink will process input events, then rAF, then it will apply styles. Your options are to read either in your event handlers which may not run, and so can't be relied up for anything but reacting to the user, or to read at the start of rAF. But there are only two places to read: input events (may fire, may not depending on what the user does), or rAF (will fire for sure if you need it to, and you can bail out of doing anything if you don't have any new data to process). I don't think we disagree on the fact that one should do reads then writes, but here's my position on it:
Finally, this approach extends to |
@paullewis
This is an important information/fact, that is not clear from the article. The example only uses one handler for one event. And the article doesn't state this as a problem, that you want to (also) solve. Also your example is quite constructed. It looks like a simple implementation of fastclick but if you know how this works you will know that there can be a 300ms gap. And you cannot
At the end: this totally depends on the use case. Which I was talking about in my original issue. For example your code is also used as backbone for lazyloaders, I can demonstrate that using the This is also important, because you have used the term debounce, which was used until then for something else. And debouncing and throttling the old school way (using At the same time you are changing the topic here. We speak about your example that uses
Yes you would need something to make it more convenient and remove the amount of closures. But what I was stating is that you only need to manage the writes (with or without fastdom.write) and that you don't need to ensure the use of one library to get the work done. As soon as all writes happen inside a rAF, reads are save anywhere else (outside rAF). I wrote a lot of components lately and they do not thrash layout. And if combined with third party components, they do not provoke layout thrashing by invalidating layout. If I would have used old fastdom.read, it would automatically and always thrash layout if combined with any JS animation framework that uses rAF.
Yeah, article was all about input events. This is why I choose my example this way. As said, I wrote a lot of components and they read either immediately on initialization or immediately inside a sync or in a throttled event handler. Due to the fact that all writes are moved, there is no layout thrashing.
If at the time of a handler is called, the styles are invalidated, a rAF/fastdom.write doesn't solve the problem either, the styles remain invalidated until the rAF callback is invoked and forces layout.
With my approach a readers queue is only needed, if you need to read right after a write/mutation (because we are in rAF land now) otherwise just do your read as you need the data/information.
This seems to be more of a feeling than a necessity. Normally you want to gather information right after they are changed/created and not right before they become invalid. |
I made a simple proof of concept. I took the following demo: The timeline of this looks like this: I haven't cared about the layout thrashing, that is already there. Instead I have focused on the additional layout thrashing (i.e. fortunately in our case only style recalculations), that happens, if you produce multiple instances with the code. This can also simulate multiple components in a website. The demo of this can be seen here: And the timeline looks like this: As expected the reads of a following instance are causing layout thrashing. As we all know this can be solved by using fastdom's read and write. But it is important to note, that this can also be solved by just using fastdom write or even rAF alone. Which is proven on this demo page: And here is the corresponding timeline: In my eyes this solution can't be underestimated, because of two things: 1. Simplicity A demo that simulates this problem by using fastdom read and write in combination with a "third party script", that uses The corresponding timeline of this looks like this: The corresponding demo, that uses the much simpler pattern (only fastdom.write/rAF only for writes), can be seen here: |
@paullewis @jakearchibald could I kindly implore either of you to check out and give your response to @aFarkas plea to reopen issue from Aug. |
The article and especially the code in "Debounce your scroll handlers" has some issues and can be improved a lot. It has to be said, that there are many different use cases to listen for scroll/resize events and not all of them can be handled with the same pattern.
Also the article Avoid large, complex layouts and layout thrashing
can be improved by establishing a new best practice, that can be easily fulfilled in practice.
So the pattern used in this article looks like this:
Let's go through it line by line.
Access to
scrollY
is a read operation in terms of style/layout and can be done at any time. While the article says you shouldn't read layout in input handler, the code just does this. This makes absolutely no sense. (I come back later to this point).While there are some old browsers out there, where the resize/scroll events go wild. All modern browsers already throttle these events and this makes totally sense. A resize/scroll that is not painted, doesn't need to be dispatched.
Because a
requestAnimationFrame
callback is always scheduled before calculating the layout/style not only the handler but also everything inside of the rAf callback is blocking the browser from rendering the next frame.This means the pattern doesn't really include any efficient throttling or debouncing.
This leaves us with the following pattern, which is async but not really debounced as described above:
A simple throttle pattern could look something like this:
Better best practice to avoid layout thrashing
The pattern also suggest to readAndWrite layout in a requestAnimationFrame. With this you don't win anything. As soon as multiple different scripts are following this advice, you end up with read -> write -> read -> write layout thrashing inside of a
requestAnimationFrame
:Of course you can say, that this has to be managed by another third party script like fastdom, but it is escapist that all scripts (especially those you don't own) in a page are using fastdom.
In fact there is a much simpler pattern to fully avoid layout thrashing, that can be established without the need of a library, that everybody has to use.
It's simple: Put all your writes inside a
requestAnimationFrame
and never read inside arequestAnimationFrame
.If you think about it, this makes much more sense.
requestAnimationFrame
main purpose was always to do animation, hence layout writes. If the browser has calculated the layout and it is not invalidated, layout reads are extremely fast no matter at which point they are called. But the point where you can be sure of it is the point right after layout calculation.requestAnimationFrame
gives you the time right before layout calculation, quite the opposite. It is the latest point to do layout reads before the browser would do layout himself, hence it is the most dangerous point to do reads. I know my explanation isn't so good, but I hope this makes it clear. See also this issue. If you have any doubts in this, just explain those. I will try to clear them.This basically means for our debounce input handler article the following patterns:
Use case: Instant visual reaction to the event
Use case: lazy reaction to the event
Use case: React after event is settled
Of course you can also add a forth pattern using a mixture of throttle and
requestIdleCallback
.The text was updated successfully, but these errors were encountered: