7

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?

MaximPro
  • 563
  • 8
  • 21
  • I am away from keyboard for a few days so I can't put an answer yet, but note that when the reflow happens is not tied by anything in your two first snippets, only the third one is sure. If what you want is to always have the transition to work, then force a reflow as exposed here:https://stackoverflow.com/questions/55134528/css-transition-doesnt-start-callback-isnt-called/55137322#55137322 If you are interested in what happens in both implementations, wait a few days I'll write about it. – Kaiido Sep 07 '21 at 08:45
  • @Kaiido I am pretty curious in your completed answer, sure I'll wait it. It's a great what you've answered to me, I am glad :) – MaximPro Sep 07 '21 at 09:12
  • @Kaiido before you complete your answer, I want to ask about one thing (I want to read it by myself). When I checked performance tab in dev-tool of chrome browser I noticed that I saw next chain of actions: animation frame fired => recalculate styles => update layer tree => paint => composite layers. And I've read the spec whatwg a little about event loop and I didn't see steps about recalculate styles and updating layer tree with compositing. Where is it all? – MaximPro Sep 07 '21 at 11:12
  • It's not part of the specs, implementers can do it as they wish. In the answer I linked to there are links where this part is actually already explained, see https://stackoverflow.com/questions/47342730/javascript-are-dom-redraw-methods-synchronous/47343090#47343090 – Kaiido Sep 07 '21 at 13:01
  • @Kaiido So how should to understand where are these steps placed? Hmm, I've read your link, but still do I understand correctly what in step `11. Update the rendering: if this is a window event loop, then:` and in substep `16. For each fully active Document in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state` exactly here 16 step means: `recalculate styles => update layer tree => paint => composite layers`... Am I correct? https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model – MaximPro Sep 07 '21 at 14:29
  • Recalculate styles and update layer tree are the reflow I talked about, so it can happen synchronously, and if it didn't happen before paint then yes it will be done at this step, but it may happen that it's not done here at all. – Kaiido Sep 07 '21 at 22:58
  • @Kaiido what do you mean here synchronously? And why may it happen that it's not done here at all? Can you explain? That is why I don't realize second demo – MaximPro Sep 07 '21 at 23:30
  • That's exactly what I did explain in https://stackoverflow.com/a/47343090/3702797 – Kaiido Sep 07 '21 at 23:32
  • @Kaiido I've read your answer, but still it gets that browsers don't guarantee "update rendering" step? Can explain shortly why we have difference in second demo between firefox and chrome? – MaximPro Sep 07 '21 at 23:36
  • I thought you could wait for that? As I told you I'm AFK now. Anyways, rAF fires **before** the next painting frame, browsers will generally wait right before the next painting frame to make the reflow but they can actually do it whenever they like, it's not in the specs. Safari for instance will do it as soon as they have a short idle time. Also to be noted that Chrome's rAF is broken and that mouse events are generally throttled to painting frames, we come back to my first comment, none of your two first snippets are sure. If you want the transition to kick in, force the reflow yourself. – Kaiido Sep 07 '21 at 23:45
  • The actual painting, (update the rendering) will fire, but it may not have to make a reflow itself if the boxes have not been dirtied since the last one. – Kaiido Sep 07 '21 at 23:46
  • @Kaiido it sounds pretty complicated...but sorry for disturbance, I am too curious about that question. Ok, ok, I'll wait, it's really interesting I am looking forward for it. – MaximPro Sep 07 '21 at 23:54

1 Answers1

3

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.

Kaiido
  • 123,334
  • 13
  • 219
  • 285