Force browser to trigger reflow while changing CSS
Asked Answered
M

4

38

I am building non-jQuery responsive image slider based on CSS3 transitions.

The structure is simple: a viewport and inside relatively positioned UL with left floated LIs.

I am facing a problem in such situation:

  1. User clicks "prev" arrow.
  2. JS appends proper LI before currently displayed LI node.
  3. For now UL has set CSS transition as none 0s linear to prevent animation changes. In this moment I decrease UL CSS left value by slider width (let's say: from 0px to -1200px) to make view the same as it was.
  4. Now I am changing UL's transition property to all 0.2s ease-out.
  5. Now I am changing UL's left property to trigger CSS3 animation. (let's say: from -1200px to 0px).

What is the problem? Browser simplifies changes and does not make any animations.

Stoyan Stefanov wrote about reflow problem at his blog here, but in this case trying to force a reflow on element doesn't work.

This is a piece of code doing this (I skipped browser prefixes for simplification):

ul.style.transition = 'none 0s linear 0s'; ul.style.left = '-600px'; ul.style.transition = 'all 0.2s ease-out'; ul.style.left = '0px';

Here is fiddle to see problem in action: http://jsfiddle.net/9WX5b/1/

Mantooth answered 9/2, 2014 at 20:56 Comment(3)
Animations only occur when a property in the CSS is changed. There's no need to apply none 0s linear nothing will animate until you change the left property.Manufacturer
Disabling transition was important due the fact that I had to invisibly fix position of older slide after appending new slide.Mantooth
Possible duplicate of "Force Reflow" in CSS transitions in BootstrapJiles
S
45

Requesting the offsetHeight of an element does everything nicely. You can force a reflow using this function and passing it the element that styles have been changed on:

function reflow(elt){
    console.log(elt.offsetHeight);
}

And call this where reflows are needed. See this example: http://jsfiddle.net/9WX5b/2/

EDIT: recently needed to do this, and wondered if there was a better way than to console.log it. You can't just write elt.offsetHeight as it's own statement, as the optimizer (Chrome's, at least) will not trigger a reflow because it is just accessing a property with no getter set, no need to even evaluate it. So, AFAIK the cheapest way to do this is void(elt.offsetHeight), as it does not know for sure if void has side effects or not. (could be overridden or something, idk).

Stonwin answered 9/2, 2014 at 21:14 Comment(7)
Thank you, you saved my day! Moreover, I used translate3d (if supported) for smoother animations and everything works as a charm in any browser. Thanks one again.Mantooth
you do not even need the console.log. just elt.offsetHeight is enoughPhotoelectric
Here you can find a long list of properties and methods that force reflow: gist.github.com/paulirish/5d52fb081b3570c81e3a I think a solution that doesn't involve console.log() is better because it does not pollute the console. @vaxquis solution is better.Mikes
Using only offsetHeight would potentially be compiled out by smart optimisers. Console.log may stop that but is also noisy and can also be stripped out. Instead you can use a keepsake variable. It's a minor memory leak but if you're clever you'll only have the one. With a writable variable you can just assign it to itself.Rumormonger
I doubt that. Browsers optimizing out getters that have side-effects would be a pretty catastrophic bug.Snath
@GlennMaynard I don't think getting offsetHeight is required to have side effects by the spec, so the browser is within its rights to optimize it out.Stonwin
void() forces evaluation of any expression passed to it, so I'm pretty sure the evaluation of that expression wouldn't get optimized-out in any standards-compliant JS engine, so using void seems the best approach here. But I am more concerned about what @Stonwin wrote about offsetHeight not being required to produce the intended effect here. In this case, the desired effect of forcing a reflow is a side-effect, not the intended use, so I would prefer a solution that is intended to force a reflow, if such a solution exists.Mckellar
P
8
function reflow( element ) {
    if ( element === undefined ) {
        element = document.documentElement;
    }
    void( element.offsetHeight );
}

It works OK with Chrome and FF, and seems to be the simplest and most portable way to do it ATM.

Pratte answered 18/7, 2017 at 18:41 Comment(0)
A
0

Even though it's already been provided in comments to the other answers, here is a pretty complete list of triggers. Note that for getters like elem.offsetWidth, there is no need to wrap it in any call. The getter will be called anyway, and if a compiler were to optimize this out, it would be terribly broken.

Triggering a reflow is sub-optimal and can be very harmful if misused.


To understand what is a reflow and why it's needed in this case, I invite you to read this answer of mine and the associated ones. TL;DR the browser will try to wait until the last minute before recalculating the whole CSSOM boxes and styles of the page and will thus only see the last status where the transition is applied, discarding the fact that it has been removed temporarily. Requesting a reflow synchronously will force the CSSOM to recalculate all the boxes and styles in the page and it will see the state where the transition wasn't set, and thus it will see that when it's added back it should perform a transition.

Now, performing a reflow means that the engine has to recalculate all the boxes of all the elements in the document. That can really computationally heavy, and if used in a loop, or in a very complex DOM structure, it can make your whole page lag out terribly.

For triggering transitions, use the Web Animations API

It didn't exist in 2014, but it is supported in all modern browsers for years now. The Web Animations API allows us to tap directly in the animation engine where the CSSOM will move its own animations and transitions.
Thanks to it, we don't need to go the slow and complex path of DOM manipulation -> CSSOM recalc -> animation engine.
You don't need to worry about style recalcs, and the API is JS friendly, returning clear Promises you can await on instead of dealing with the multitude of CSS animation-x events.

To reproduce OP's fiddle with the Web Animations API, you can simply do:

function makeAnimation()
{
    const ul = document.getElementsByTagName('ul')[0];
    ul.animate(
      [ // The "keyframes" of our animation/transition
        { left: "-600px", },
        { left: "-0px", }
      ],
      { duration: 200/* ms */, easing: "ease-out" }
    );
}
.viewport { width: 600px; height: 300px; }
ul { list-style: none; margin: 0; padding: 0; position: relative; width: 250%;  }
ul li { display: block; float: left; width: 600px; height: 300px; line-height: 300px; font-size: 30px; text-align: center; }
<div class="viewport">
    <ul>
        <li style="background: lightblue; color: red">1</li>
        <li style="background:gray; color: black;">2</li>
    </ul>
</div>
<button onclick="makeAnimation()">Make animation</button>

What if I really need to trigger a reflow?

That should probably be its own Q/A, but in such a case, the best is to batch all your reflow triggers in a single place and make sure nothing will dirty the box model. This is far from being easy, as it requires you have full control over what modifies the DOM. But the basic idea is that in whatever stage of the Event-Loop your code runs, you make all your DOM modifications, without calling a single trigger (beware some are sneaky).
Then you wait until the next ResizeObserver callback. Indeed, these callbacks are called after the browser did its own recalc (specs, step 16). So this means that if we do wait until there, and then call elem.offsetWidth, the browser will already have the given information cached and won't need to perform a new recalc & reflow. However for this to stand, you must not dirty the DOM during that phase. One strategy I use is thus to have a two phases handler in the ResizeObserver callbacks where I execute a first batch of callbacks that will gather and return all the computed values needed, and then another batch of callbacks that will perform the DOM manips based on the computed values.

This strategy allows me to fire most of the time only 2 reflows per frame (the one from the browser, and the one for my scripts). But even when you know very well what triggers a reflow, you can always discover new sneaky ones, and there are cases like scrollTo() which will both trigger a reflow and dirty the boxes for which you can't do anything...

Asperse answered 30/3 at 1:39 Comment(0)
J
-1

requestAnimationFrame should be suitable for most use cases and it's much cleaner than relying on undocumented side-effects of DOM APIs:

el.style.transition = 'none';
requestAnimationFrame(() => {
  el.style.transition = 'all 1s ease';
});
Jackpot answered 29/3 at 15:4 Comment(1)
It fires before the browser's reflow so unless you are already between the raf callbacks and the painting of the previous frame that won't work (and when it will you'll be one frame late)Asperse

© 2022 - 2024 — McMap. All rights reserved.