How does really work rendering in browser (event loop) - javascript

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?

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.

Related

requestAnimationFrame and getComputedStyle don't work as expected (following along Jake Archibalds "In the Loop" speech)

I am watching and following along Jake Archibald's speech about the Event Loop. At some point he attemps to move a box, first to 1000px and then back to 500px. You can also watch in the video here:
Jake Archibald: In The Loop - JSConf.Asia (22:10)
He offers two solutions and I couldn't get any of them to work for me, even though (I think) I am doing exactly the same. The first solution is this:
button.addEventListener("click", () => {
box.style.transform = "translateX(1000px)";
box.style.transition = "transform 1s ease-in-out";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
box.style.transform = "translateX(500px)";
});
});
});
And the second "hacky" one, as he says, is this:
button.addEventListener("click", () => {
box.style.transform = "translateX(1000px)";
box.style.transition = "transform 1s ease-in-out";
getComputedStyle(box).transform;
box.style.transform = "translateX(500px)";
});
On both occasions the box just moves to 500px, while it should first move to 1000px and then back to 500px.
Am I missing something? Am I doing something wrong? Or have things changed that much since 2018 so that this solution doesn't work anymore?
P.S. I am using Chrome Version 102.0.5005.61 on Ubuntu 22.04.
What the video shows is wrong here.
For this explanation I will use the "hacky" way, since it seems easier to understand as it's synchronous and thus we don't have to wonder when the reflow happens. (And if you know what you're doing it's not actually more hacky than any async way).
For a transition to work, we need an initial value, a destination and a flag telling we're ok to transition a given property change.
When this flag is raised, any change to the value that can be transitioned will be used as the destination, and the current value will be used as the initial value of our transition.
In the example, before we click the button, we only have an initial value (none, i.e translateX(0px)).
button.addEventListener("click", () => {
Then we set both the new transform and transition values.
box.style.transform = "translateX(1000px)";
box.style.transition = "transform 1s ease-in-out";
At this point, the CSS layout hasn't been updated, it doesn't know we did change anything.
The next line, which forces the reflow, will take care of that.
getComputedStyle(box).transform;
And here the CSS engine sees that it has the transition flag for transform raised AND that the transform value has changed. It thus initiates a transition from the current value (0px) to the new one (1000px).
But even before it can start rendering that transition,
box.style.transform = "translateX(500px)";
changes the destination to be 500px. So it will update the transition to go from wherever it is at this point (still 0px) to that new destination (500px).
The same obviously happens with the async version, except that when we set the new value to 500px our element will already have moved a bit (by ~17px on a 60Hz monitor, while it should have only moved by half of that).
This isn't very noticeable with this value because it's only one frame and the speed difference between 500px/s and 1000px/s isn't very noticeable in a single frame, but if you have a much bigger difference, you can see it better:
const box = document.querySelector(".box");
const button = document.querySelector(".button");
button.addEventListener("click", () => {
// Forcing a huge value here will make the transition
// to start from much farther on the right
// because it will travel a quite big distance on the first frame
// and then start the new transition from the current position
// Depending on your monitor's refresh rate you may even see it
// coming from the right.
box.style.transform = "translateX(500000px)";
box.style.transition = "transform 1s ease-in-out";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
box.style.transform = "translateX(500px)";
});
});
});
// So we can try multiple times
document.querySelector(".reset").onclick = (evt) => {
box.style.transform = "none";
box.style.transition = "none";
box.style.offsetWidth;
}
.box {
width: 50px;
height: 50px;
border: 1px solid;
}
<button class="button">click me</button>
<button class="reset">reset</button>
<div class="box"></div>
So once again I'll play the "hacky" advocate here. Using the synchronous way, you do have control over what happens. Just be very careful that you don't pollute the CSS layout anymore after you've forced that reflow and you'll be fine, the browser won't recalculate it in the rendering step.
As far as I can remember this has always been the expected and actual behavior, I highly doubt this has changed since this talk and you can even retrieve some comments from 2019 under the video that talk about this not working as explained in the video.
To get the expected behavior, you'd need to set the transition along with the setting of the destination value, after the reflow that calculated the new initial position.
const box = document.querySelector(".box");
const button = document.querySelector("button");
button.addEventListener("click", () => {
box.style.transform = "translateX(1000px)";
getComputedStyle(box).transform;
box.style.transform = "translateX(500px)";
box.style.transition = "transform 1s ease-in-out";
});
// So we can try multiple times
document.querySelector(".reset").onclick = (evt) => {
box.style.transform = "none";
box.style.transition = "none";
box.style.offsetWidth;
}
.box {
width: 50px;
height: 50px;
border: 1px solid;
}
<button class="button">click me</button>
<button class="reset">reset</button>
<div class="box"></div>
or
const box = document.querySelector(".box");
const button = document.querySelector("button");
button.addEventListener("click", () => {
box.style.transform = "translateX(1000px)";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
box.style.transform = "translateX(500px)";
box.style.transition = "transform 1s ease-in-out";
});
});
});
// So we can try multiple times
document.querySelector(".reset").onclick = (evt) => {
box.style.transform = "none";
box.style.transition = "none";
box.style.offsetWidth;
}
.box {
width: 50px;
height: 50px;
border: 1px solid;
}
<button class="button">click me</button>
<button class="reset">reset</button>
<div class="box"></div>

CSS transform: scale very shaky/choppy/jerky in Safari

I'm trying to implement a scroll based shrinking effect on on a div element. As the user scrolls, the div shrinks in height (using scale) until it finally disappears. I'm using React to render the HTML and styled-components for the CSS.
I'm having problems with the performance of this effect, specifically only with Safari, Version 13.1 (15609.1.20.111.8). In Safari, the transition effect is very janky and does not happen smoothly.
Chrome and Firefox work perfectly.
Here is the React component:
const MyComponent = React.forwardRef((_, ref) => {
const [scaleOffset, setScaleOffset] = React.useState(1)
const onScroll = React.useCallback(debounceWindowEventCallback(() => {
const paddingOffset = window.scrollY - ref.current.offsetTop
if (paddingOffset >= 0) setScaleOffset(1 - (paddingOffset / 100))
}), [ref]);
React.useEffect(() => {
if (scaleOffset <= 0) {
window.removeEventListener('scroll', onScroll)
console.log('Border has been fully shrunk')
}
}, [onScroll, scaleOffset])
React.useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])
return (
<RootElement ref={ref}>
<ShrinkBorderTop scaleOffset={scaleOffset} />
</RootElement>
);
});
And the relevant styled components are:
const ShrinkBorderTop = styled.div`
position: absolute;
background-color: white;
transition: transform .175s ease-out;
width: 100%;
height: 200px;
transform: ${({ scaleOffset }) => `scale(1, ${scaleOffset})`};
`
What I've tried:
I initially thought its because my component was rendering too much (every scroll event) so I implemented the debounceWindowEventCallback function which uses window.requestAnimationFrame to ensure that there's only a single paint per frame. Unfortunately, that didn't help.
I then tried adding will-change: transition, transform, height, width; to ShrinkBorderTop, after googling the problem. This also didn't help.
Then I tried adding perspective: 1000; backface-visibility: hidden; to ShrinkBorderTop, this also didn't work.
I'm stumped as to what could be causing this problem, in my view its quite a standard animation and I've applied all the performance boosts I'm aware of, in addition to scale being hardware accelerated by default, but the problem still persists, only in safari.
What could be causing this?

CSS transition won't start on blur screen

I'm using react js.
I try to run the simple code:
setTimeout(() => {
setTimeout(() => {
const element = document.getElementById(‘circle’)
element.style.backgroundColor = ‘red’
}, 3000)
}, 3000)
The CSS of 'circle' is just:
width: 100px;
height: 100px;
border-radius: 50%;
background-color: blue;
transition: background-color 2s;
I run the code and immediate change tab or minimize the screen, waiting more then 6 seconds and go back to the page and then the transition start. For some reason the transition not run if screen not in focus.
Any help guys???
The behavior you describe depending on the browser because inactive tabs have low priority execution.
While you performing a "JS Animation" you may want to use requestAnimation.
A better approach may use a "CSS Animation" with transition-delay.
Use this code why are using Apostrophe mark in code.
<script>
setTimeout(() => {
setTimeout(() => {
const element = document.getElementById('circle')
element.style.backgroundColor = 'red'
}, 3000)
}, 3000)
</script>
Actually it is not really help me.
The solution i made is to listen to 'focus' and 'blur' and stop any animations process and timers on blur and continue from last point at focus.

JavaScript window resize event is slow. How to optimize window resize event

When I click on the Maximize button of the browser window, a function I wrote that will execute when the window is resized, does not work properly I think because JavaScript window resize event is running slow. It worked when I used the mouse to resize the window. Also, when I tried to change between portrait mobile to landscape mobile it is also slow.
Faced a problem with using with window.addEventListener('resize', aFunction), page gets slow. These events are generated multiple times per second, and if the event handler takes too much time, the browser won’t catch with redrawing the page.
function aFuntion() {
let div1 = document.querySelector('.div1');
let div2 = document.querySelector('.div2');
let diff = div1.clientHeight - div2.clientHeight;
div2.style.top = diff + 'px';
};
// Call the function.
// This worked!
aFuntion();
// Call the function when users resize the window.
// This worked!
window.addEventListener('resize', aFuntion);
// Users click on the Maximize button of the window.
// The function does not work properly!
// Portrait mobile to Landscape mobile.
// The function does not work properly!
.div1 {
position: relative;
}
.div2 {
position: absolute;
width: 300px;
top: 0;
transition: all 0.5s ease;
}
#media only screen and (max-width: 1024px) {
.div2 {
width: 200px;
}
}
<div class="div1">
<div class="div2"></div>
</div>
I removed transition: all 0.5s ease; and it worked for a while now it is not working again.
So, I know now it is because of window.addEventListener('resize', function) is running slow.
Try this:
var timeout = false;
window.addEventListener('resize', function() {
clearTimeout(timeout);
timeout = setTimeout(aFuntion, 200);
});
Read: https://bencentra.com/code/2015/02/27/optimizing-window-resize.html
try with document.addEventListener('resize', aFuntion);

How to pause a css animation, and then continue running from the pause point?

I'm playing a CSS animation with infinite iterations, until some point I pausing it from java script (using technique from here).
At later time I resume the animation. The problem is that the animation is restarting from the beginning point, and not from the last point before the pause.
Does there is a way to continue the animation from the pause point, without jumping to the start?
Edit: Between the pause and resume I also change the animation duration, see the demo.
You should see some 'jumping'. Need explanation about this strange behavior.
http://jsfiddle.net/zhang6464/MSqMQ/
Just switch animation-play-state css property to be paused, according to the article you supplied(http://css-tricks.com/restart-css-animation/
I've just started to dig into JS myself, so there are surely better ways to do this, but how about something like this - Demo (Unprefixed version).
CSS
#box {
position: relative;
top: 20px;
left: 20px;
width: 100px;
height: 100px;
background: #393939;
animation: move 2s linear infinite;
animation-play-state: running;
}
#keyframes move {
100% {
transform: rotate(360deg);
}
}
JS:
(function () {
var box = document.getElementById('box');
box.onclick = function () {
if (box.style.animationPlayState === "paused") {
box.style.animationPlayState = "running";
} else {
box.style.animationPlayState = "paused";
}
};
})();
Based on if the animation is running or paused I change the animation-play-state onclick.
EDIT: Added the css code.

Categories