Does a hidden/transparent element impact rendering performance? - javascript

So I have a Polymer app that I am writing. I have written a non-polymer web-component for a loading overlay that I can show whilst Polymer is loading and when the app Websocket is connecting/reconnecting.
Here is an exert of some of the CSS I have to give an indication of what I am doing:
.overlay {
background: #000;
bottom: 0;
height: 100%;
left: 0;
opacity: 0;
pointer-events: none;
position: fixed;
right: 0;
transition: opacity 0.2s;
top: 0;
width: 100%;
z-index: 9999999;
}
.overlay[opened] {
opacity: 0.8;
pointer-events: auto;
}
.loader {
display: none;
}
.overlay[opened] .loader {
display: block;
}
Now this overlay and the CSS based loader animation I have is only used when I load the application realistically, however if the WebSocket were to disconnect it would be shown too.
My question is, for performance reasons, should I be removing the element from the DOM entirely and add it back if its required? Does the fact that the overlay is completely transparent when not in use and the loader animation is hidden mean they have no impact on drawing performance?
Note: I am looking to avoid the "don't micro-optimise" answer if possible ;)

TL;DR:
In general, a rendered element affects page performance when changes to it trigger repaint on subsequent elements in DOM or when it triggers resize on its parent(s), as resize can get expensive from being fired up to 100 times/second, depending on device.
As long as changes to your element do not trigger repaint on subsequent elements in DOM tree, the difference between having it rendered, hidden behind some opaque element (or above the content, with opacity:0 and pointer-events:none) and having it not displayed at all is insignificant.
Changes to your element will not trigger repaint on anything but itself, because it has position:fixed. The same would be true if it had position:absolute or if the changes to it would be made through properties that do not trigger repaint on subsequent siblings, like transform and opacity.
Unless the loader is really heavy on the rendering engine (which is rarely the case — think WebGL loaders with 3d scenes, materials and lights mapping — in which case it would be better to not display it when not shown to the user), the difference would be so small that the real challenge is to measure this difference, performance wise.
In fact, I would not be surprised if having it rendered and only changing its opacity and pointer-events properties is not, overall, less expensive than toggling its display property, because the browser doesn't have to add/remove it from DOM each time you turn it on/off. But, again, the real question is: how do we measure it?
Edit: Actually, I made a small testing tool, with 10k modals. I got the following results, in Chrome, on Linux:
`opacity` average: 110.71340000000076ms | count: 100
`display` average: 155.47145000000017ms | count: 100
... so my assumption was correct: display is more expensive overall.
The opacity changes are mostly around 110ms with few exceptions, while the display changes are faster when nodes are removed but slower when added.
Feel free to test it yourself, in different browsers, on different systems:
$(window).on('load', function () {
let displayAvg = 0, displayCount = 0,
opacityAvg = 0, opacityCount = 0;
for (let i = 0; i < 10000; i++) {
$('body').append($('<div />', {
class: 'modal',
html:'10k × modal instances'
}))
}
$(document)
.on('click', '#display', function () {
$('.modal').removeClass('opacity');
let t0 = performance.now();
$('.modal').toggleClass('display');
setTimeout(function () {
let t1 = performance.now();
displayAvg += (t1 - t0);
console.log(
'`display` toggle took ' +
(t1 - t0) +
'ms \n`display` average: ' +
(displayAvg / ++displayCount) +
'ms | count: ' +
displayCount
);
})
})
.on('click', '#opacity', function () {
$('.modal').removeClass('display');
let t0 = performance.now();
$('.modal').toggleClass('opacity');
setTimeout(function () {
let t1 = performance.now();
opacityAvg += (t1 - t0);
console.log(
'`opacity` + `pointer-events` toggle took ' +
(t1 - t0) +
'ms \n`opacity` average: ' +
(opacityAvg / ++opacityCount) +
'ms | count: ' +
opacityCount
);
});
})
});
body {
margin: 0;
}
.buttons-wrapper {
position: relative;
z-index: 1;
margin-top: 3rem;
}
.modal {
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
padding: 1rem;
}
.modal.display {
display: none;
}
.modal.opacity {
opacity: 0;
pointer-events: none;
}
.as-console-wrapper {
z-index: 2;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="buttons-wrapper">
<button id="display">Toggle `display`</button>
<button id="opacity">Toggle `opacity` + `pointer-events`</button>
</div>
But this average is for 10k elements. Divide it by 10k and it's virtually no difference at all: we're talking less than 0.45% of a millisecond.

If an element is animated with 'Animation' property and its duration is infinite browser will continuously repaint the site and that will affect the site performance and lower the FPS.
However hiding elements with properties such as Opacity:0; will not do the trick because element is still in CSSOM rendering tree queue.
Visibility:hidden; and display:none; should do the trick based on CSSOM construction browser doesn't render hidden elements such as display:none; & visibility:hidden

Related

CSS property not set before transition (via javascript/typescript) [duplicate]

I have a large game project that used extensive jquery in its code. Some time ago I stripped out all of the jquery and replaced it with pure JS, but the one thing I had trouble with was replacing the .animation calls for projectiles in the game.
It appeared that I should replace them with CSS transitions, and the game needed to know when the transition was done, so I needed to add a callback to the transition. All well and good, except when I assigned new location values for the projectile, the transition was skipped entirely and no callback was called. For some ungodly reason, it started working if I wrapped the change to its css position in a SetTimeout for 1ms--and even then, sometimes it would still skip the transition.
Now it usually works, except about one time in 10 the transition will play but then the callback will not be called. I don't know why the settimeout helps to be there, and I don't know why the callback sometimes doesn't work. Can anyone help me understand?
let tablehtml = '<div id="'+animid+'" style="position: absolute; left: ' + ammocoords.fromx + 'px; top: ' + ammocoords.fromy + 'px; background-image:url(\'graphics/' + ammographic.graphic + '\');background-repeat:no-repeat; background-position: ' + ammographic.xoffset + 'px ' + ammographic.yoffset + 'px; transition: left '+duration+'ms linear 0s, top '+duration+'ms linear 0s;"><img src="graphics/spacer.gif" width="32" height="32" /></div>';
document.getElementById('combateffects').innerHTML += tablehtml;
let animdiv = document.getElementById(animid);
animdiv.addEventListener("transitionend", function(event) {
FinishFirstAnimation();
}, false);
setTimeout(function() { Object.assign(animdiv.style, {left: ammocoords.tox+"px", top: ammocoords.toy+"px" }); }, 1); // THIS IS A TOTAL KLUDGE
// For some reason, the transition would not run if the 1ms pause was not there. It would skip to the end, and not
// fire the transitionend event. This should not be necessary.
For a comprehensive explanation of why this happens, see this Q/A, and this one.
Basically, at the time you set the new style the browser still has not applied the one set inline, your element's computed style still has its display value set to ""
, because it's what elements that are not in the DOM default to.
Its left and top computed values are still 0px, even though you did set it in the markup.
This means that when the transition property will get applied before next frame paint, left and top will already be the ones you did set, and thus the transition will have nothing to do: it will not fire.
To circumvent it, you can force the browser to perform this recalc. Indeed a few DOM methods need the styles to be up to date, and thus browsers will be forced to trigger what is also called a reflow.
Element.offsetHeight getter is one of these method:
let tablehtml = `
<div id="spanky"
style="position: absolute;
left: 10px;
top: 10px;
background-color:blue;
width:20px;
height:20px;
transition: left 1000ms linear 0s, top 1000ms linear 0s;">
</div>`;
document.body.innerHTML += tablehtml;
let animdiv = document.getElementById('spanky');
animdiv.addEventListener("transitionend", function(event) {
animdiv.style.backgroundColor='red';
}, false);
// force a reflow
animdiv.offsetTop;
// now animdiv will have all the inline styles set
// it will even have a proper display
animdiv.style.backgroundColor='green';
Object.assign(animdiv.style, {
left: "100px",
top: "100px"
});
But really, the best is probably to directly use the Web Animation API, which is supported almost everywhere (but in long dead IE). This will take care of doing the right thing at the right time.
let tablehtml = `
<div id="spanky"
style="
position: absolute;
background-color:blue;
width:20px;
height:20px;
"
</div>`;
document.body.innerHTML += tablehtml;
let animdiv = document.getElementById("spanky");
const anim = animdiv.animate([
{ left: "10px", top: "10px" },
{ left: "100px", top: "100px" }
],
{
duration: 1000,
fill: "forwards"
});
anim.finished.then(() => {
animdiv.style.backgroundColor = "green";
});
it has to do with the timing of when the new element is actually "painted" ...
I know this is a kludge too but ...
the one way I've found to guarantee 100% success is to wait just under two frames (at 60fps that's about 33.333ms - but the setTimeout in browsers these days has a artificial "fudge" added due to spectre or something - anyway ...
requestAmiationFrame(() => requestAnimationFrame() => { ... your code ...})) does the same thing except it could be as little as 16.7ms delay, i.e. just over one frame
let tablehtml = `
<div id="spanky"
style="position: absolute;
left: 10px;
top: 10px;
background-color:blue;
width:20px;
height:20px;
transition: left 1000ms linear 0s, top 1000ms linear 0s;">
</div>`;
document.body.innerHTML += tablehtml;
let animdiv = document.getElementById('spanky');
animdiv.addEventListener("transitionend", function(event) {
animdiv.style.backgroundColor='red';
}, false);
requestAnimationFrame(() => requestAnimationFrame(() => {
animdiv.style.backgroundColor='green';
Object.assign(animdiv.style, {
left: "100px",
top: "100px"
});
}));
having a single requestAnimationFrame failed about 1 in 10 for me, but the double request makes it impossible to fail
I was able to get rid of setTimeout() by following the Web Animation Approach suggested by Kaiido.
But for me anim.finished.then(()=>{}); is not invoked after the frame is actually painted. It took 2ms to invoke this callback.
For my case anim.onfinish=()=>{} makes sure the frame is painted and it took 16ms to invoke the callback.

My css animation is consuming a lot of resources

I'm trying to make a starring night with twinkling stars in css3 + Javascript, however, my animation is consuming a lot of CPU, the main animation:
#for $i from 0 through 400 {
.star:nth-child(#{$i}) {
$star-size: (random() * (1-4) +4) + px;
top: (random(100)) + vh;
left: (random(100)) + vw;
width: $star-size;
height: $star-size;
animation: blinker 1.2s alternate infinite ease-in-out;
animation-delay: (random(30) / 10) + s;
transform: scale(0.2);
}
}
#keyframes blinker {
100% {
transform: scale(1);
}
}
the full code: https://jsfiddle.net/sam7krx0/
is there any way to make this code perform better?
Edit:
tried with translateZ(0) and with will-change: transform but the animation still being rendered by the CPU.
https://jsfiddle.net/8hn97kcx/2/
Edit 2:
It seems that firefox might be the problem, while testing on chrome the animation uses way less CPU.
Edit 3:
profile of the fiddle above running on firefox developer edition 69.0b4:
firefox profile
CPU usage:
Have you tried using the will-change property - this helps the browser know about the change and offload it to the compositor if possible.
The OP code was horrendously inefficient in that it uses 400+ uniquely generated selectors. So the bulk of the processing time involves maintaining the CSS animation loop and looking up 400+ classes on each alternation of said CSS animation. This is a rare case wherein class selectors are a burden and not useful. Since each s.star needs these unique styles, it would take less computing power to generate the CSS property values on a template literal and then assign it to the tag as an inline-style. (See Demo)
Besides doing away with ridiculously huge .class lists on a bloated stylesheet, the demo makes full use of a documentFragment. DOM operations are expensive on resources (imagine 400+ tags being appended to one location). Doing everything on the fragment, then finally to the DOM by 👍appending documentFragment just once and 400 .star are in the DOM👍. The OP code on the other hand 👎will append 400 s.star one at a time... that's 400+ DOM operations.👎
Also on the OP code it is deceiving as to the size of the actual CSS. SCSS, a post-processor is used, so what looks like 8 lines of weird looking CSS is actually 👎3200 lines of CSS👎 after it has been compiled and cached by the browser. The CSS in the demo is what it appears to be ...👍9 lines👍 for .star selector.
/**| documentFragment
- The only global variable points to a documentFragment not attached to the DOM.
- Use fragment as you would a document object when moving, creating, destroying,
appending, detaching, etc... HTML element tags from and to the DOM. Doing so will
greatly improve processing times when adding 400+ uniquely styled tags.
- When all .star tags have been created, modified, and appended to the fragment --
only the fragment itself needs to be appended to the DOM instead of 400 tags.
*/
var fragment = document.createDocumentFragment();
/**| randomRange(min, max, integer = false)
#Params: min [number].....: The minimum
max [number].....: The maximum
integer [boolean]: default is false which results will be floats.
If true then results will be integers.
Utility function that will return a random number from a given range of consecutive
numbers.
*/
const randomRange = (min, max, integer = false) => {
let numbers = integer ? {
min: Math.ceil(min),
max: Math.floor(max)
} : {
min: min,
max: max
};
return Math.random() * (numbers.max - numbers.min + 1) + numbers.min;
};
/**| starGenerator(limit)
#Params: limit [number]: The number of s.star to generate.
A generator function that creates s.star tags. Assigning individual tag properties
and setting randomly determined values would involve a ton of unique selectors.
To avoid a ton of lookups in a CSS stylesheet a mile long, it's easier to create and
maintain one template literal of the CSS properties interpolated with random values.
Each s.star would be assigned an inline-style of five CSS properties/values by one
statement via `.cssText` property.
*/
function* starGenerator(limit) {
let iteration = 0;
while (iteration < limit) {
iteration++;
const star = document.createElement("s");
star.classList.add("star");
let properties = `
width: ${randomRange(1, 4)}px;
height: ${randomRange(1, 4)}px;
top: ${randomRange(0, 100, true)}vh;
left: ${randomRange(0, 100, true)}vw;
animation-delay: ${randomRange(1, 30, true) / 10}s`;
star.style.cssText = properties;
yield star;
}
return fragment;
}
/**| nightfall(selector, limit = 400)
#Params: selector [string]: Target parent tag
limit [number].. : The maximum number of s.star to generate.
Interface function that facilitates DOM procedures with minimal presence in DOM.
*/
const nightfall = (selector, limit = 400) => {
const base = document.querySelector(selector);
base.classList.add('sky');
for (let star of starGenerator(limit)) {
fragment.appendChild(star);
}
return base.appendChild(fragment);
};
// Call nightfall() passing the selector "main"
nightfall("main");
.sky {
position: relative;
background: #000;
height: 100vh;
overflow: hidden;
}
.star {
display: block;
position: absolute;
animation: twinkle 1.2s alternate infinite ease-in-out;
transform: scale(0.2);
border-radius: 50%;
background: #fff;
box-shadow: 0 0 6px 1px #fff;
z-index: 2;
text-decoration: none;
}
#keyframes twinkle {
100% {
transform: scale(1);
}
}
<main></main>
That's because the rendering is done by CPU which can be a loose in performance. There is an option in CSS to run such an animation on GPU.
Your snippet adjusted
#for $i from 0 through 400 {
.star:nth-child(#{$i}) {
$star-size: (random() * (1-4) +4) + px;
transform: translateY((random(100)) + vh) translateX((random(100)) + vw) translateZ(0);
width: $star-size;
height: $star-size;
animation: blinker 1.2s alternate infinite ease-in-out;
animation-delay: (random(30) / 10) + s;
transform: scale(0.2);
}
}
#keyframes blinker {
100% {
transform: scale(1);
}
}
It's very important to add translateZ because only 3D renderings are done by GPU.
Doing animations on GPU is also called accelerated animations, please check this helpful article for more information about: https://www.sitepoint.com/introduction-to-hardware-acceleration-css-animations/
it's not only problem with you code.
it's also from your CPU ability, trying to upgrade your CPU and RAM to perform better.
sometimes you can't build mid - high animation in low spec computer.

Changing an HTML element's style in JavaScript with its CSS transition temporarily disabled isn't reliably functioning [duplicate]

This question already has answers here:
How can I force WebKit to redraw/repaint to propagate style changes?
(33 answers)
Closed 4 years ago.
Currently I am working on an animation for a website which involves two elements having their position changed over a period of time and usually reset to their initial position. Only one element will be visible at a time and everything ought to run as smoothly as possible.
Before you ask, a CSS-only solution is not possible as it is dynamically generated and must be synchronised. For the sake of this question, I will be using a very simplified version which simply consists of a box moving to the right. I shall be referring only to this latter example unless explicitly stated for the remainder of this question to keep things simple.
Anyway, the movement is handled by the CSS transition property being set so that the browser can do the heavy lifting for that. This transition must then be done away with in order to reset the element's position in an instant. The obvious way of doing so would be to do just that then reapply transition when it needs to get moving again, which is also right away. However, this isn't working. Not quite. I'll explain.
Take a look at the JavaScript at the end of this question or in the linked JSFiddle and you can see that is what I'm doing, but setTimeout is adding a delay of 25ms in between. The reason for this is (and it's probably best you try this yourself) if there is either no delay (which is what I want) or a very short delay, the element will either intermittently or continually stay in place, which isn't the desired effect. The higher the delay, the more likely it is to work, although in my actual animation this causes a minor jitter because the animation works in two parts and is not designed to have a delay.
This does seem like the sort of thing that could be a browser bug but I've tested this on Chrome, Firefox 52 and the current version of Firefox, all with similar results. I'm not sure where to go from here as I have been unable to find this issue reported anywhere or any solutions/workarounds. It would be much appreciated if someone could find a way to get this reliably working as intended. :)
Here is the JSFiddle page with an example of what I mean.
The markup and code is also pasted here:
var box = document.getElementById("box");
//Reduce this value or set it to 0 (I
//want rid of the timeout altogether)
//and it will only function correctly
//intermittently.
var delay = 25;
setInterval(function() {
box.style.transition = "none";
box.style.left = "1em";
setTimeout(function() {
box.style.transition = "1s linear";
box.style.left = "11em";
}, delay);
}, 1000);
#box {
width: 5em;
height: 5em;
background-color: cyan;
position: absolute;
top: 1em;
left: 1em;
}
<div id="box"></div>
Force the DOM to recalculate itself before setting a new transition after reset. This can be achieved for example by reading the offset of the box, something like this:
var box = document.getElementById("box");
setInterval(function(){
box.style.transition = "none";
box.style.left = "1em";
let x = box.offsetLeft; // Reading a positioning value forces DOM to recalculate all the positions after changes
box.style.transition = "1s linear";
box.style.left = "11em";
}, 1000);
body {
background-color: rgba(0,0,0,0);
}
#box {
width: 5em;
height: 5em;
background-color: cyan;
position: absolute;
top: 1em;
left: 1em;
}
<div id="box"></div>
See also a working demo at jsFiddle.
Normally the DOM is not updated when you set its properties until the script will be finished. Then the DOM is recalculated and rendered. However, if you read a DOM property after changing it, it forces a recalculation immediately.
What happens without the timeout (and property reading) is, that the style.left value is first changed to 1em, and then immediately to 11em. Transition takes place after the script will be fihished, and sees the last set value (11em). But if you read a position value between the changes, transition has a fresh value to go with.
Instead of making the transition behave as an animation, use animation, it will do a much better job, most importantly performance-wise and one don't need a timer to watch it.
With the animation events one can synchronize the animation any way suited, including fire of a timer to restart or alter it.
Either with some parts being setup with CSS
var box = document.getElementById("box");
box.style.left = "11em"; // start
box.addEventListener("animationend", animation_ended, false);
function animation_ended (e) {
if (e.type == 'animationend') {
this.style.left = "1em";
}
}
#box {
width: 5em;
height: 5em;
background-color: cyan;
position: absolute;
top: 1em;
left: 1em;
animation: move_me 1s linear 4;
}
#keyframes move_me {
0% { left: 1em; }
}
<div id="box"></div>
Or completely script based
var prop = 'left', value1 = '1em', value2 = '11em';
var s = document.createElement('style');
s.type = 'text/css';
s.innerHTML = '#keyframes move_me {0% { ' + prop + ':' + value1 +' }}';
document.getElementsByTagName('head')[0].appendChild(s);
var box = document.getElementById("box");
box.style.animation = 'move_me 1s linear 4';
box.style.left = value2; // start
box.addEventListener("animationend", animation_ended, false);
function animation_ended (e) {
if (e.type == 'animationend') {
this.style.left = value1;
}
}
#box {
width: 5em;
height: 5em;
background-color: cyan;
position: absolute;
top: 1em;
left: 1em;
}
<div id="box"></div>

Change width of element without reflow

I have a music player with an animated bar that displays the current position in the song. It is rendered with requestAnimationFrame and works by changing the width style of the div to X%, where X is the percentage of time through the current song.
This causes huge CPU use in Chrome I believe due to the constant reflow work being done each frame. What are other options I can use to eliminate reflows and reduce CPU?
Two other requirements: this web page is a web UI over a back end music server. It's not using any HTML5 media elements. As such, the page may be loaded when the song is already partially over, so the position will not always animate between 0 and 100.
The below fiddle shows up at about 30% CPU on my machine, which seems a bit high to animate a rectangle.
var pos = 0;
var s = document.getElementById('i');
f = function() {
window.requestAnimationFrame(f);
pos += .03;
s.style.width = pos + '%';
}
f();
#i {
background-color: red;
position: absolute;
}
<div id="i">
</div>
There are a number of ways you could make a pure CSS progress bar that won’t cause a relayout, here are a few examples:
animation - http://jsbin.com/yoqabe/edit?html,css,js,output
I think one of the most performant ways would be to use an animation to control the background position of a linear-gradient. The only downside is that you can only play/pause the animation.
background-position - http://jsbin.com/veyibe/edit?html,css,js,output
If you need the ability to update the position with JS, then I would suggest updating the background-position of a gradient and applying CSS transitions, debouncing to avoid updating too quickly.
translateX: http://jsbin.com/zolurun/edit?html,js,output
You could also use CSS transforms to slide the progress bar inside of a container, which should also avoid a repaint.
These links might also be useful:
List of CSS layout, paint, and composite triggers: http://csstriggers.com
Debounce info: https://davidwalsh.name/javascript-debounce-function
You can consider using other CSS properties which don't require layout opearations, such as background-size.
And use CSS animations instead of requestAnimationFrame.
var bar = document.getElementById('i');
function playSong(currentTime, duration) {
bar.style.animationDuration = duration + 's';
bar.style.animationDelay = - currentTime + 's';
}
playSong(3, 10);
#i {
height: 1em;
background-image: linear-gradient(red, red);
background-repeat: no-repeat;
animation: bar linear;
}
#keyframes bar {
from { background-size: 0% 100%; }
to { background-size: 100% 100%; }
}
<div id="i"></div>
If you use position: absolute or position: fixed on the progress bar itself, it should prevent large reflows on the page.
Use timeupdate, The time indicated by the element's currentTime attribute has changed.
Try this:
var audio = document.getElementById("audio");
function updateProgress() {
var progress = document.getElementById("progress");
var value = 0;
if (audio.currentTime > 0) {
value = Math.ceil((100 / audio.duration) * audio.currentTime);
}
progress.style.width = value + "%";
}
audio.addEventListener("timeupdate", updateProgress, false);
#progressBar {
border: 1px solid #aaa;
color: #fff;
width: 295px;
height: 20px;
}
#progress {
background-color: #ff0000; // red
height: 20px;
display: block;
height: 100%;
width: 0;
}
<div id="progressBar"><span id="progress"></span>
</div>
<audio id="audio" controls>
<source src="http://www.w3schools.com/tags/horse.ogg" type="audio/ogg" />
<source src="http://www.w3schools.com/tags/horse.mp3" type="audio/mpeg" />Your browser does not support the audio element.
</audio>
The script you present is not very relevant to the one you desire, you animate on requestAnimationFrame but in reality you will animate every time the "song percentage" changes.
Assuming that you have a function (e.g. getSongPer()) that returns the current percentage of played song:
var oldPos, pos = 0;
var s = document.getElementById('i');
f = function() {
oldPos = pos;
pos = getSongPer();
if(oldPos !== pos){
s.style.width = pos + '%';
}
if(pos<100){
window.requestAnimationFrame(f);
}
}
f();
I didn't test it, but I expect it to be lighter, also, the performance will be affected by the precision of the percentage, e.g. there will be about 100 animation changes if you have zero digit precision and around ten times more for every digit after.
CSS:
#progress-bar {
background-color: red;
height: 10px;
width: 100%;
transform-origin: 0 0;
}
JS:
'use strict'
var progressBar = document.getElementById('progress-bar')
function setProgress(percentage) {
requestAnimationFrame(function () {
progressBar.style.transform = 'scaleX(' + percentage + '%)'
})
}
setProgress(10)
When setting the width to 100% you get a full width colored bar.
Then we can apply the scale transform to set the width of the bar without reflowing.
But oh, it scales to the middle. We can fix that by setting the origin of the transform to the top left corner using transform-origin: x y, with x and y being 0.
Then we wrap the style change in requestAnimationFrame to let the browser optimize when to apply the change.
Bam! You have a performant zero reflow progress bar.

Trigger CSS transition on appended element

As this question observes, immediate CSS transitions on newly-appended elements are somehow ignored - the end state of the transition is rendered immediately.
For example, given this CSS (prefixes omitted here):
.box {
opacity: 0;
transition: all 2s;
background-color: red;
height: 100px;
width: 100px;
}
.box.in { opacity: 1; }
The opacity of this element will be set immediately to 1:
// Does not animate
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.addClass('in');
I have seen several ways of triggering the transition to get the expected behaviour:
// Does animate
var $b = $('<div>')
.addClass('box b')
.appendTo('#wrapper');
setTimeout(function() {
$('.b').addClass('in');
},0);
// Does animate
var $c = $('<div>')
.addClass('box c')
.appendTo('#wrapper');
$c[0]. offsetWidth = $c[0].offsetWidth
$c.addClass('in');
// Does animate
var $d = $('<div>')
.addClass('box d')
.appendTo('#wrapper');
$d.focus().addClass('in');
The same methods apply to vanilla JS DOM manipulation - this is not jQuery-specific behaviour.
Edit - I am using Chrome 35.
JSFiddle (includes vanilla JS example).
Why are immediate CSS animations on appended elements ignored?
How and why do these methods work?
Are there other ways of doing it
Which, if any, is the preferred solution?
The cause of not animating the newly added element is batching reflows by browsers.
When element is added, reflow is needed. The same applies to adding the class. However when you do both in single javascript round, browser takes its chance to optimize out the first one. In that case, there is only single (initial and final at the same time) style value, so no transition is going to happen.
The setTimeout trick works, because it delays the class addition to another javascript round, so there are two values present to the rendering engine, that needs to be calculated, as there is point in time, when the first one is presented to the user.
There is another exception of the batching rule. Browser need to calculate the immediate value, if you are trying to access it. One of these values is offsetWidth. When you are accessing it, the reflow is triggered. Another one is done separately during the actual display. Again, we have two different style values, so we can interpolate them in time.
This is really one of very few occasion, when this behaviour is desirable. Most of the time accessing the reflow-causing properties in between DOM modifications can cause serious slowdown.
The preferred solution may vary from person to person, but for me, the access of offsetWidth (or getComputedStyle()) is the best. There are cases, when setTimeout is fired without styles recalculation in between. This is rare case, mostly on loaded sites, but it happens. Then you won't get your animation. By accessing any calculated style, you are forcing the browser to actually calculate it.
Using jQuery try this (An Example Here.):
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.css('opacity'); // added
$a.addClass('in');
Using Vanilla javaScript try this:
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity; // added
e.className += ' in';
Brief idea:
The getComputedStyle() flushes all pending style changes and
forces the layout engine to compute the element's current state, hence
.css() works similar way.
About css()from jQuery site:
The .css() method is a convenient way to get a style property from the
first matched element, especially in light of the different ways
browsers access most of those properties (the getComputedStyle()
method in standards-based browsers versus the currentStyle and
runtimeStyle properties in Internet Explorer) and the different terms
browsers use for certain properties.
You may use getComputedStyle()/css() instead of setTimeout. Also you may read this article for some details information and examples.
Please use the below code, use "focus()"
Jquery
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.focus(); // focus Added
$a.addClass('in');
Javascript
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e).focus(); // focus Added
e.className += ' in';
I prefer requestAnimationFrame + setTimeout (see this post).
const child = document.createElement("div");
child.style.backgroundColor = "blue";
child.style.width = "100px";
child.style.height = "100px";
child.style.transition = "1s";
parent.appendChild(child);
requestAnimationFrame(() =>
setTimeout(() => {
child.style.width = "200px";
})
);
Try it here.
#Frizi's solution works, but at times I've found that getComputedStyle has not worked when I change certain properties on an element. If that doesn't work, you can try getBoundingClientRect() as follows, which I've found to be bulletproof:
Let's assume we have an element el, on which we want to transition opacity, but el is display:none; opacity: 0:
el.style.display = 'block';
el.style.transition = 'opacity .5s linear';
// reflow
el.getBoundingClientRect();
// it transitions!
el.style.opacity = 1;
Anything fundamentally wrong with using keyframes for "animate on create"?
(if you strictly don't want those animations on the initial nodes, add another class .initial inhibitin animation)
function addNode() {
var node = document.createElement("div");
var textnode = document.createTextNode("Hello");
node.appendChild(textnode);
document.getElementById("here").appendChild(node);
}
setTimeout( addNode, 500);
setTimeout( addNode, 1000);
body, html { background: #444; display: flex; min-height: 100vh; align-items: center; justify-content: center; }
button { font-size: 4em; border-radius: 20px; margin-left: 60px;}
div {
width: 200px; height: 100px; border: 12px solid white; border-radius: 20px; margin: 10px;
background: gray;
animation: bouncy .5s linear forwards;
}
/* suppres for initial elements */
div.initial {
animation: none;
}
#keyframes bouncy {
0% { transform: scale(.1); opacity: 0 }
80% { transform: scale(1.15); opacity: 1 }
90% { transform: scale(.9); }
100% { transform: scale(1); }
}
<section id="here">
<div class="target initial"></div>
</section>
Rather than trying to force an immediate repaint or style calculation, I tried using requestAnimationFrame() to allow the browser to paint on its next available frame.
In Chrome + Firefox, the browser optimizes rendering too much so this still doesn't help (works in Safari).
I settled on manually forcing a delay with setTimeout() then using requestAnimationFrame() to responsibly let the browser paint. If the append hasn't painted before the timeout ends the animation might be ignored, but it seems to work reliably.
setTimeout(function () {
requestAnimationFrame(function () {
// trigger the animation
});
}, 20);
I chose 20ms because it's larger than 1 frame at 60fps (16.7ms) and some browsers won't register timeouts <5ms.
Fingers crossed that should force the animation start into the next frame and then start it responsibly when the browser is ready to paint again.
setTimeout() works only due to race conditions, requestAnimationFrame() should be used instead. But the offsetWidth trick works the best out of all options.
Here is an example situation. We have a series of boxes that each need to be animated downward in sequence. To get everything to work we need to get an animation frame twice per element, here I put once before the animation and once after, but it also seems to work if you just put them one after another.
Using requestAnimationFrame twice works:
Works regardless of how exactly the 2 getFrame()s and single set-class-name step are ordered.
const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));
async function run() {
for (let i = 0; i < 100; i++) {
const box = document.createElement('div');
document.body.appendChild(box);
// BEFORE
await getFrame();
//await delay(1);
box.className = 'move';
// AFTER
await getFrame();
//await delay(1);
}
}
run();
div {
display: inline-block;
background-color: red;
width: 20px;
height: 20px;
transition: transform 1s;
}
.move {
transform: translate(0px, 100px);
}
Using setTimeout twice fails:
Since this is race condition-based, exact results will vary a lot depending on your browser and computer. Increasing the setTimeout delay helps the animation win the race more often, but guarantees nothing.
With Firefox on my Surfacebook 1, and with a delay of 2ms / el, I see about 50% of the boxes failing. With a delay of 20ms / el I see about 10% of the boxes failing.
const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));
async function run() {
for (let i = 0; i < 100; i++) {
const box = document.createElement('div');
document.body.appendChild(box);
// BEFORE
//await getFrame();
await delay(1);
box.className = 'move';
// AFTER
//await getFrame();
await delay(1);
}
}
run();
div {
display: inline-block;
background-color: red;
width: 20px;
height: 20px;
transition: transform 1s;
}
.move {
transform: translate(0px, 100px);
}
Using requestAnimationFrame once and setTimeout usually works:
This is Brendan's solution (setTimeout first) or pomber's solution (requestAnimationFrame first).
# works:
getFrame()
delay(0)
ANIMATE
# works:
delay(0)
getFrame()
ANIMATE
# works:
delay(0)
ANIMATE
getFrame()
# fails:
getFrame()
ANIMATE
delay(0)
The once case where it doesn't work (for me) is when getting a frame, then animating, then delaying. I do not have an explanation why.
const delay = (d) => new Promise(resolve => setTimeout(resolve, d));
const getFrame = () => new Promise(resolve => window.requestAnimationFrame(resolve));
async function run() {
for (let i = 0; i < 100; i++) {
const box = document.createElement('div');
document.body.appendChild(box);
// BEFORE
await getFrame();
await delay(1);
box.className = 'move';
// AFTER
//await getFrame();
//await delay(1);
}
}
run();
div {
display: inline-block;
background-color: red;
width: 20px;
height: 20px;
transition: transform 1s;
}
.move {
transform: translate(0px, 100px);
}
Edit: the technique used in the original answer, below the horizontal rule, does not work 100% of the time, as noted in the comments by mindplay.dk.
Currently, if using requestAnimationFrame(), pomber's approach is probably the best, as can be seen in the article linked to in pomber's answer. The article has been updated since pomber answered, and it now mentions requestPostAnimationFrame(), available behind the Chrome flag --enable-experimental-web-platform-features now.
When requestPostAnimationFrame() reaches a stable state in all major browsers, this will presumably work reliably:
const div = document.createElement("div");
document.body.appendChild(div);
requestPostAnimationFrame(() => div.className = "fade");
div {
height: 100px;
width: 100px;
background-color: red;
}
.fade {
opacity: 0;
transition: opacity 2s;
}
For the time being, however, there is a polyfill called AfterFrame, which is also referenced in the aforementioned article. Example:
const div = document.createElement("div");
document.body.appendChild(div);
window.afterFrame(() => div.className = "fade");
div {
height: 100px;
width: 100px;
background-color: red;
}
.fade {
opacity: 0;
transition: opacity 2s;
}
<script src="https://unpkg.com/afterframe/dist/afterframe.umd.js"></script>
Original answer:
Unlike Brendan, I found that requestAnimationFrame() worked in Chrome 63, Firefox 57, IE11 and Edge.
var div = document.createElement("div");
document.body.appendChild(div);
requestAnimationFrame(function () {
div.className = "fade";
});
div {
height: 100px;
width: 100px;
background-color: red;
}
.fade {
opacity: 0;
transition: opacity 2s;
}

Categories

Resources