Change width of element without reflow - javascript

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.

Related

Infinite moving text with scrolling

I want make a moving infinite text like this text(We are trusted by over 28,000...) in this link . I already tried something but I have not reached exactly what I want. It is close but not infinite.
<h1 class="deneme display-1 " id="movingtext">We are trusted by over 28,000 clients to power stunning websites.We are trusted by over 28,000 clients to power stunning websites.</h1>
window.addEventListener('scroll', () => {
var elemen = document.getElementById("movingtext");
elemen.style = "left:-450px"
var rect = elemen.getBoundingClientRect();
var rect1 = rect.left;
var scrolled = window.scrollY;
var deg = (rect1 + scrolled) / 1.8;
elemen.style = "left:" + deg + "px";
console.log(rect.left, rect.right);
})
As you can see in the following screenshots this is not infinite either:
first row with "We are trusted by over 28,000..."
and second row
It's just that on normal screen you can't reach the end of the element.
You can accomplish something similar by adding an listener on scroll and transforming the element you want to be moved retative to window.pageYOffset value.
Something like:
window.addEventListener('scroll', () => {
const movingtext = document.getElementById("movingtext");
const scrolled = window.pageYOffset;
//feel free to play with this value to change the speed of the transform ( the `* 3` part)
const left = scrolled * 3;
movingtext.style.transform = `translate3d(-${left}px, 0px, 0px)`;
})
.wrapper {
height: 300vh;
padding-top: 100vh;
max-width: 100vw;
overflow: hidden;
}
.infinite-text {
display: inline-block;
overflow: hidden;
white-space: nowrap;
}
<div class="wrapper">
<h1 class="infinite-text" id="movingtext">We are trusted by over 28,000 clients to power stunning websites.We are trusted by over 28,000 clients to power stunning websites.</h1>
</div>
Please note that this is a draft. You might want to play with sizes and speed of transform or other attributes.
Please note that for this example the container has max-width: 100vw; and overflow: hidden (so it won't display a scrollbar for the element that is bigger than the screen), and the element itself has white-space: nowrap;

Pinning Elements with Debounced Scroll Event for Performance

What is the right way to smoothly pin an element according to scroll position?
I tried debouncing a scroll listener for performance but the pinning is not accurate. Even with debouncing set to 10ms it's not smooth and the element doesn't snap cleanly to its initial position.
var scrolling = false;
var stickPosY = 100;
var heights = [];
$(".element").each( function(index) {
heights[index] = $(".element[data-trigger=" + index + "]").offset().top;
});
function pin() {
if ( !$("#aside").hasClass("fixed") ) {
var stickyLeft = $("#aside").offset().left;
var stickyWidth = $("#aside").outerWidth();
var stickyTop = $("#aside").offset().top - stickPosY;
$("#aside").addClass("fixed");
$("#aside").css({"left": stickyLeft, "top": stickyTop, "width": stickyWidth});
}
}
function unpin() {
$("#aside").css({"left": "", "top": "", "width": ""});
$("#aside").removeClass("fixed")
}
$( window ).scroll( function() {
scrolling = true;
});
setInterval( function() {
if ( scrolling ) {
scrolling = false;
var y = window.scrollY;
console.log(y);
// PIN SIDEBAR
y > stickPosY ? pin() : unpin();
//TRIGGERS
for (var i=0; i < heights.length; i++) {
if (y >= heights[i]) {
$('.element[data-trigger="' + i + '"]').addClass("blue");
}
else {
$('.element[data-trigger="' + i + '"]').removeClass("blue");
}
}
}
}, 250 );
Here's my Pen
I tried to use scrollMagic for the project on a scene with a pin and additional triggers but the scrolling wasn't very smooth. So I'm trying to rebuild it with a stripped-down version and debounced listeners. Is this approach possible, or should I rather try to optimize my scrollMagic scene?
As James points out, you can just use position: sticky as one option, but that doesn't work in older browsers and its uses are limited to simpler situations in newer browsers, so I'll continue with the JS solution assuming you want to go that route.
There is a lot going on in your JS, and I think you are probably overcomplicating things, so I will give you a few basics to consider.
When you are toggling things based on scroll, either toggle inline styles or a class, but not both. I would recommend toggling a class because it allows you to have one function that can work on multiple screen sizes (i.e., you can use media queries to change the behavior of your toggled class based on screen size). Also it keeps all your styles in one place instead of having them split between your JS and your stylesheet.
Try to keep the work you're doing while scrolling as minimal as possible. For example, cache references to elements in variables outside your scroll function so you're not continually looking them up every time you scroll a pixel. Avoid loops inside scroll functions.
Using setInterval is not generally the recommended approach for increasing performance on scroll functions. All that is going to do is run a function every X amount of time, all the time, whether you're scrolling or not. What you really want to do is rate-limit your scroll function directly. That way, if you scroll a long ways real fast your function will only be called a fraction of the total times it would otherwise be called, but if you scroll a short distance slowly it will still be called a minimum number of times to keep things looking smooth, and if you don't scroll at all then you're not calling your function at all. Also, you probably want to throttle your function in this case, not debounce it.
Consider using the throttle function from Underscore.js or Lodash.js instead of inventing your own because those ones are highly performant and guaranteed to work across a wide variety of browsers.
Here is a simple example of sticking an element to the top of the screen on scroll, throttled with Lodash. I'm using a 25ms throttle, which is about the maximum amount I'd recommend for keeping things looking smooth where you won't really notice the delay in the element sticking/unsticking as you scroll past your threshold. You could go down to as little as 10ms.
$(function() {
$(window).on('scroll', _.throttle(toggleClass, 25));
const myThing = $('#my-thing');
const threshold = $('#dummy-1').height();
function toggleClass() {
const y = window.scrollY;
if (y > threshold) {
myThing.addClass('stuck')
} else {
myThing.removeClass('stuck');
}
}
});
#dummy-1 {
height: 150px;
background-color: steelblue;
}
#dummy-2 {
height: 150px;
background-color: gold;
}
#my-thing {
width: 300px;
height: 75px;
background-color: firebrick;
position: absolute;
top: 150px;
left: 0;
}
#my-thing.stuck {
position: fixed;
top: 0;
}
body {
margin: 0;
height: 2000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.0.0/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="dummy-1"></div>
<div id="dummy-2"></div>
<div id="my-thing"></div>
You could try fixed or sticky CSS positioning:
#element {
position: fixed;
top: 80px;
left: 10px;
}
Position: fixed would keep the element always at 80px from the top and 10px from the left edge regardless of scroll position.
#element{
position: sticky;
top: 0;
right: 0;
left: 0;
}
This is from a project of mine. The element is a nav bar. It sits below a header bar, so when you are at the top of the page, you see the header then the nav below it, and as you scroll down, the header moves off screen but the nav sticks at the top and is always visible.

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>

Does a hidden/transparent element impact rendering performance?

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

Show a series of images on scroll

The closest solution I found is Show div on scrollDown after 800px.
I'm learning HTML, CSS, and JS, and I decided to try to make a digital flipbook: a simple animation that would play (ie, load frame after frame) on the user's scroll.
I figured I would add all the images to the HTML and then use CSS to "stack them" in the same position, then use JS or jQuery to fade one into the next at different points in the scroll (ie, increasing pixel distances from the top of the page).
Unfortunately, I can't produce the behavior I'm looking for.
HTML (just all the frames of the animation):
<img class="frame" id="frame0" src="images/hand.jpg">
<img class="frame" id="frame1" src="images/frame_0_delay-0.13s.gif">
CSS:
body {
height: 10000px;
}
.frame {
display: block;
position: fixed;
top: 0px;
z-index: 1;
transition: all 1s;
}
#hand0 {
padding: 55px 155px 55px 155px;
background-color: white;
}
.frameHide {
opacity: 0;
left: -100%;
}
.frameShow {
opacity: 1;
left: 0;
}
JS:
frame0 = document.getElementById("frame0");
var myScrollFunc = function() {
var y = window.scrollY;
if (y >= 800) {
frame0.className = "frameShow"
} else {
frame0.className = "frameHide"
}
};
window.addEventListener("scroll", myScrollFunc);
};
One of your bigger problems is that setting frame0.className = "frameShow" removes your initial class frame, which will remove a bunch of properties. To fix this, at least in a simple way, we can do frame0.className = "frame frameShow", etc. Another issue is that frame0 is rendered behind frame1, which could be fixed a variety of ways. ie. Putting frame0's <img> after frame1, or setting frame0's CSS to have a z-index:2;, and then setting frame0's class to class="frame frameHide" so it doesn't show up to begin with. I also removed the margin and padding from the body using CSS, as it disturbs the location of the images. I have made your code work the way I understand you wanted it to, here is a JSFiddle.
It depends on your case, for example, in this jsFiddle 1 I'm showing the next (or previous) frame depending on the value of the vertical scroll full window.
So for my case the code is:
var jQ = $.noConflict(),
frames = jQ('.frame'),
win = jQ(window),
// this is very important to be calculated correctly in order to get it work right
// the idea here is to calculate the available amount of scrolling space until the
// scrollbar hits the bottom of the window, and then divide it by number of frames
steps = Math.floor((jQ(document).height() - win.height()) / frames.length),
// start the index by 1 since the first frame is already shown
index = 1;
win.on('scroll', function() {
// on scroll, if the scroll value equal or more than a certain number, fade the
// corresponding frame in, then increase index by one.
if (win.scrollTop() >= index * steps) {
jQ(frames[index]).animate({'opacity': 1}, 50);
index++;
} else {
// else if it's less, hide the relative frame then decrease the index by one
// thus it will work whether the user scrolls up or down
jQ(frames[index]).animate({'opacity': 0}, 50);
index--;
}
});
Update:
Considering another scenario, where we have the frames inside a scroll-able div, then we wrap the .frame images within another div .inner.
jsFiddle 2
var jQ = $.noConflict(),
cont = jQ('#frames-container'),
inner = jQ('#inner-div'),
frames = jQ('.frame'),
frameHeight = jQ('#frame1').height(),
frameWidth = jQ('#frame1').width() + 20, // we add 20px because of the horizontal scroll
index = 0;
// set the height of the outer container div to be same as 1 frame height
// and the inner div height to be the sum of all frames height, also we
// add some pixels just for safety, 20px here
cont.css({'height': frameHeight, 'width': frameWidth});
inner.css({'height': frameHeight * frames.length + 20});
cont.on('scroll', function() {
var space = index * frameHeight;
if (cont.scrollTop() >= space) {
jQ(frames[index]).animate({'opacity': 1}, 0);
index++;
} else {
jQ(frames[index]).animate({'opacity': 0}, 0);
index--;
}
});
** Please Note that in both cases all frames must have same height.

Categories

Resources