Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does really work rendering in browser (event loop)

I've created simple demos, let's get started...

Should to say what we have to use chrome and firefox for comparison

Demo 1:

block.addEventListener("click", () => {
    block.style.transform = "translateX(500px)";
    block.style.transition = "";
    block.style.transition = "transform 1s ease-in-out";
    block.style.transform = "translateX(100px)";
});
.main {
  width: 100px;
  height: 100px;
  background: orange;
  }
<div id="block" class="main"></div>

In both browsers, we'll not see any changes

Demo 2:

block.addEventListener("click", () => {
    block.style.transform = "translateX(500px)";
    block.style.transition = "";
    requestAnimationFrame(() => {
      block.style.transition = "transform 1s ease-in-out";
      block.style.transform = "translateX(100px)";
    });
});
.main {
  width: 100px;
  height: 100px;
  background: orange;
  }
<div id="block" class="main"></div>

In chrome we'll see animation, in firefox we'll see another thing. Need to mention that firefox conforms actions from video of Jake Archibald in the Loop. But not in case with chrome. Seems that firefox conforms to spec, but not chrome

Demo 2 (alternate):

block.addEventListener("mouseover", () => {
    block.style.transform = "translateX(500px)";
    block.style.transition = "";
    requestAnimationFrame(() => {
      block.style.transition = "transform 1s ease-in-out";
      block.style.transform = "translateX(100px)";
    });
});
.main {
  width: 100px;
  height: 100px;
  background: orange;
  }
<div id="block" class="main"></div>

Now we see that chrome works correctly, but firefox does same thing as chrome was doing on Demo 2. They has changed by theirs places I've also tested events: mouseenter, mouseout, mouseover, mouseleave, mouseup, mousedown. The most interesting thing that the last two ones work same in chrome and firefox and they are both incorrect, I think.

In conclusion: It seems what these two UAs differently treats events. But how do they do?

Demo 3:

block.addEventListener("click", () => {
    block.style.transform = "translateX(500px)";
    block.style.transition = "";
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        block.style.transition = "transform 1s ease-in-out";
        block.style.transform = "translateX(100px)";
      });
    });
});
.main {
  width: 100px;
  height: 100px;
  background: orange;
  }
<div id="block" class="main"></div>

Here we see that firefox works as well as chrome, how this expects, by the Archibald's words. But do you remember demo 2, both versions, why their behaviour is so different?

like image 468
MaximPro Avatar asked Sep 06 '21 04:09

MaximPro


People also ask

How does a browser event loop work?

The Event Loop has one simple job — to monitor the Call Stack and the Callback Queue. If the Call Stack is empty, the Event Loop will take the first event from the queue and will push it to the Call Stack, which effectively runs it. Such an iteration is called a tick in the Event Loop.

Do browsers have an event loop?

A browser might have multiple event loops (for JS, for DOM, etc), but the concept is the same, yes.

Can you explain the event loop?

The event loop is a constantly running process that monitors both the callback queue and the call stack. In this example, the timeout is 0 second, so the message 'Execute immediately. ' should appear before the message 'Bye!'

How is event loop implemented?

On each iteration, the event loop tries to synchronously pull out the next callback from the queue. If there is no callback to execute at the moment, pop() blocks the main thread. When the callback is ready, the event loop executes it. The execution of a callback always happens synchronously.


1 Answers

TL;DR; if you want your code to work the same everywhere force a reflow yourself after you set the values you want as initial ones.

block.addEventListener("click", () => {
    block.style.transform = "translateX(500px)";
    block.style.transition = "";
    requestAnimationFrame(() => {
      // if you want your transition to start from 0
      block.style.transform = "translateX(0px)";
      // force reflow
      document.body.offsetWidth;
      block.style.transition = "transform 1s ease-in-out";
      block.style.transform = "translateX(100px)";
    });
});
.main {
  width: 100px;
  height: 100px;
  background: orange;
  }
<div id="block" class="main"></div>

What you are stumbling upon here is called the reflow. I already wrote about it in other answers, but basically this reflow is the calculations of all the boxes in the page needed to determine how to paint every elements.
This reflow (a.k.a layout or recalc) can be an expensive operation, so browsers will wait as much as they can before doing this.
However when this happens is not part of the event loop's specifications. The only constraint is that when the ResizeObserver's notifications are to be fired this recalc has been done.
Though implementations can very well do it before if they wish (Safari for instance will do it as soon as it has a small idle time), or we can even force it by accessing some properties that do require an updated layout.

So, before this layout has been recalculated, the CSSOM won't even see the new values you passed to the element style, and it will treat it just like if you did change these values synchronously, i.e it will ignore all the previous values.
Given both Firefox and Chrome do wait until the last moment (before firing the ResizeObserver's notifications) to trigger the reflow, we can indeed expect that in these browsers your transition would start from the initial position (translate(0)) and that the intermediate values would get ignored. But once again, this is not true for Safari.

So what happens here in Chrome? I'm currently on my phone and can't make extensive tests, but I can already see that the culprit is the line setting the transition, setting it first will "fix" the issue.

block.addEventListener("click", () => {
    block.style.transform = "translateX(500px)";
    block.style.transition = "transform 1s ease-in-out";
    requestAnimationFrame(() => {
      block.style.transform = "translateX(100px)";
    });
});
.main {
  width: 100px;
  height: 100px;
  background: orange;
  }
<div id="block" class="main"></div>

Since I have to make a guess I'd say setting the transition probably makes the element switch its rendering path (e.g from CPU rendered to GPU rendered) and that they will force a reflow doing so. But this is still just a guess without proper testings. The best would be to open an issue to https://crbug.com since this is probably not the intended behavior.

As to why you have different behavior from different events it's probably because these events are firing at different moments, for instance at least mousemove will get throttled to painting frames, I'd have to double check for mousedown and mouseup though.

like image 122
Kaiido Avatar answered Sep 27 '22 19:09

Kaiido