I have an element I'm applying some basic transitions to based on scroll position. It works smoothly as expected in Safari and Firefox, but scrolling in Chrome is very choppy.
$(document).ready(function($) {
var divUp = $('.fade');
var divDown = $('.fade-down');
$(window).on('scroll', function() {
var st = $(this).scrollTop();
divUp.css({
'top' : -(st/6)+"px",
'opacity' : 1 - st/400
});
divDown.css({
'opacity' : 1 - st/400
});
});
});
I commented out each CSS property individually, but Chrome is choppy either way. The top property is moving a relatively positioned element.
How can I achieve the desired effect while still making Chrome's JS engine happy? Thanks in advance for any feedback.
You're experiencing layout thrashing.
Changing an element's top property invalidates the current layout. Usually this prompts the browser to re-compute the layout asynchronously (i.e. not immediately).
However, calling scrollTop forces the browser to re-layout synchronously. Because you call it in a scroll event handler, this happens repeatedly in a very short space of time. This sequence of DOM write-reads is a known cause of jank.
To improve performance you need to prevent layout thrashing. Changing the CSS transform (and opacity) properties does not invalidate the browser's layout - they only require a composite, which is much faster.
If you animate a transform: translateY instead of top the browser won't need to compute costly calculations on every animation frame:
divUp.css({
'transform': 'translateY( ' + (-(st/6)) + 'px)',
'opacity': 1 - st/400
});
You can help the browser optimise for the transition by setting the CSS will-change property:
.your-div {
will-change: transform;
}
Further reading:
Jank free - Articles on improving web app performance
CSS Triggers - Lists the steps that browsers need to take when each CSS property is changed
Related
first of all I want to say that it's not about content jumping!
I have a navbar and a sidebar which both have absolute position. after user scrolls 100 pixels I change both of them to fixed. but an odd action happens (not always!). wrappers of navbar and sidebar flush for a second. I tested it with different browsers and it does not depend on browser. I tried to reproduce the situation in this fiddle:
https://jsfiddle.net/addxmkgj/
(resize the screen as large as possible it happens in large screens)
-- Edit --
https://codepen.io/anon/pen/dJKBPe
codepen link added too.
Causes
Scrolling can generate scroll events quickly and handlers may need to either throttle scroll events to some extent (e.g. perform code action after scrolling has stopped) or be fairly lightweight functions that can execute quickly.
In addition scroll event handling is not synchronized with page update: if the mouse wheel initiates downward scrolling, scrolling can continue after the wheel is released (and similarly with touch event scrolling). The browser can scroll below a top position of 100px before scroll event handling has had a chance to catch up and change the positioning.
The result is the header jumps down from being partially off-screen to occupy a fixed position at top of screen. The faster the scroll action (or the busier the browser is) the more likely it is that jumping will be noticeable.
A secondary effect in desktop browsing is that when the side bar panel scrolls upwards past top of screen and moves down again, a visible patch of white screen "flashes" momentarily below the side bar before fixed positioning takes effect.
Experimental Remedies
Flashing of the side bar can be reduced but not necessarily fully eliminated, by increasing the height of the container. Changing the height to 150% with visible overflow met with some success:
.side-bar {
position: absolute;
height: 150%;
... /* more declarations */
This may or may not conflict with application requirements.
Some mitigation of navbar jumping can be achieved by using requestAnimationFrame call backs to monitor scrollTop values and change positioning as necessary. This does not use scroll event handling as such:
$(document).ready(function() {
$(window).resize(function() {
if( $(window).width() > 850) {
$('.navbar').css('display', 'block');
} else {
$('.navbar').css('display', 'none');
}
});
scrollTo(0, 0);
var num = 100;
var bAbsolute = true;
function checkScroll() {
var newTop = $(window).scrollTop();
if( bAbsolute && newTop >= num) {
$('.navbar').css('position', 'fixed');
$('.navbar').css('top', '0');
$('.side-bar').css('position', 'fixed');
$('.side-bar').css('top', '0');
bAbsolute = false;
}
if( !bAbsolute && newTop < num) {
$('.navbar').css('position', 'absolute');
$('.side-bar').css('position', 'absolute');
$('.navbar').css('top', '100px');
$('.side-bar').css('top', '100px');
bAbsolute = true;
}
requestAnimationFrame( checkScroll);
}
requestAnimationFrame( checkScroll)
});
This code showed an improvement in jump reduction but was not perfect. It is not particularly a JQuery solution and calls requestAnimationFrame directly.
One option, of course, is to do nothing given browser timing constraints.
Update
This MDN guide for Scroll linked effects explains the root cause problem better than I was able to:
most browsers now support some sort of asynchronous scrolling .... the visual scroll position is updated in the compositor thread and is visible to the user before the scroll event is updated in the DOM and fired on the main thread ... This can cause the effect to be laggy, janky, or jittery — in short, something we want to avoid.
So the absolutely positioned elements can scroll off screen (to some extent) before scroll handlers are notified of a new scroll position.
The solution going forward is to use sticky positioning (see the scroll effects guide above or the CSS position guide. However position:sticky swaps between relative and fixed position so the HTML would need redesigning to accommodate this.
Sticky positioning is also leading edge technology at January 2018, and not yet recommended for production use on MDN. A web search for "JQuery support sticky position" revealed a choice of JQuery plugin support.
Recommendation
Potentially the best-case compromise may be to redesign the HTML to use sticky positioning and include a JQuery plugin that uses native support when available or a polyfill when not - site visitors with supporting browsers will get the best experience, those with older browsers will get functional support.
var AddFootnoteScrollIndicator = function(){
$('.mobileFootnote').on('scroll touchmove', function (event) {
var scrollTop = that.$mobileFootnote.scrollTop();
if (scrollTop <= 20){
var opacity = 1 - (scrollTop/20);
$('.scroll-down-indicator').css({'opacity': opacity });
}
});
};
As the user scrolls down, the indicator slowly fades out until it is gone. They scroll back up, the indicator slowly re-appears. They stop in the middle, the indicator is half-visible.
Code works fine, but modifying the opacity via .css() seems expensive. Is there a more clever way of doing this via css or...
I don't want to delay the .on() polling because the animation needs to respond quickly to the scroll.
Any ideas?
When it comes to scroll events, modifying the css via javascript is the only way to go. There is not a way with pure CSS to detect scroll positions like you can with media queries and screen sizes.
The jquery css() function is setting the element.style.opacity property under the hood. You are only one short abstraction layer from the actual element property, so it is not "expensive".
The most costly part of that call would be the $('.scroll-down-indicator') selector, as it has to perform a DOM traversal to find elements with the class name.
I have a problem with my micro website. When I scroll, it's nice and smooth in all browsers except Safari. When I do scroll in Safari, the content div jumps or moves frequently (it should stay in place) and makes the scrolling look choppy. Do you have any idea what could be wrong?
This is the website:
http://beta.dynamicdust.com
I haven't checked to see how my answer compares to Jack's, but I think the problem is that Safari attempts to be very power efficient. As a result, it is hesitant to enable hardware acceleration unless it needs to. A common trick that people use to force hardware acceleration is to place
-webkit-transform: translate3d(0, 0, 0);
into the css for the divs which are moving. I tried it with the content class and it seemed a little better. You might try applying it to the other layers as well.
EDIT: I applied it to the left and right text holder divs as well, and the page seems just as smooth as Chrome now.
I took a look and did see the "choppy" scrolling you mentioned (looking at it more, it was hit or miss - sometimes it was smooth, other times it was VERY choppy).
It seems you've got some performance issues with your parallax callback on Safari (though it wouldn't surprise me if it's some buggy implementation with Safari...)
One thing I would recommend is taking a look at requestAnimationFrame for webkit. For a test, I wrapped the logic to update the offsets in a raf (and cached the window.pageYOffset value) and it seemed smoother on my end.
function parallax(e) {
window.webkitRequestAnimationFrame(function() {
var offset = window.pageYOffset;
a.style.top = (offset / 2) + "px";
b.style.top = (offset / 2) + "px";
textbox.style.top =- (offset * 0.7) + "px";
textbox2.style.top =- (offset * 0.7) + "px";
});
}
To be honest, you could probably use raf for all browsers (if they support it).
Another trick people use when animating elements is to accelerate the layer that the element you are animating is on. There are a few ways to do this, but the easiest is to use -webkit-transition and set translateZ(0). It wouldn't hurt to add the two additional lines as well:
-webkit-backface-visibility: hidden;
-webkit-perspective: 1000;
Also, I noticed that some elements you offset (using style) are position: relative - Personally, I'd say that any element that's to be animated should be position: absolute. This will remove the element from the DOM, and offsetting it won't cause reflows to surrounding elements (which may contribute to your choppiness).
Edit - one other thing I noticed is that "choppiness/weirdness" happens when you encounter rubberbanding on safari (my guess are the negative values). That might be something you'll want to look at as well.
Good luck!
Just jotting this down, as I came across this today with an overflow auto element:
The solution was to add this rule to whatever element the scrollbar shows up on:
-webkit-overflow-scrolling: touch;
More detail can be found here:
https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-overflow-scrolling
Elements with heavy transitions on hover and active state can result in rendering issues.
In our case we had box-shadow transitions on some elements that were applied on hover. When the user scrolled the page and swiped the effected element, the transition got triggered. The browser then had to do the hard work of rendering the transition while scrolling.
This may come as a huge surprise to some people but I am having an issue with the IE browser when I am using the $(window).scroll method.
My goal:
I would like to have the menu located on the left retain it's position until the scroll reaches > y value. It will then fix itself to the top of the page until the scroll returns to a < y value.
My error:
Everything seems just fine in Chrome and Firefox but when I go to Internet Explorer it would seem the browser is moving #scroller every time the scroll value changes, this is causing a moving/flickering event.
If someone could point me to a resource or give me a workaround for this I would be very grateful!
Here is a fiddle:
http://jsfiddle.net/CampbeII/nLK7j/
Here is a link to the site in dev:
http://squ4reone.com/domains/ottawakaraoke/Squ4reone/responsive/index.php
My script:
$(window).scroll(function () {
var navigation = $(window).scrollTop();
if (navigation > 400) {
$('#scroller').css('top',navigation - 220);
} else {
$('#scroller').css('top',183);
$('#scroller').css('position','relative');
}
});
You might want to take a look at the jQuery Waypoints plugin, it lets you do sticky elements like this and a lot more.
If you want to stick with your current method, like the other answers have indicated you should toggle fixed positioning instead of updating the .top attribute in every scroll event. However, I would also introduce a flag to track whether or not it is currently stuck, this way you are only updating the position and top attributes when it actually make the transition instead of every scroll event. Interacting with the DOM is computationally expensive, this will take a lot of load off of the layout engine and should make things even smoother.
http://jsfiddle.net/WYNcj/6/
$(function () {
var stuck = false,
stickAt = $('#scroller').offset().top;
$(window).scroll(function () {
var scrollTop = $(window).scrollTop();
if (!stuck && scrollTop > stickAt) {
$('#scroller').css('top', 0);
$('#scroller').css('position','fixed');
stuck = true;
} else if (stuck && scrollTop < stickAt) {
$('#scroller').css('top', stickAt);
$('#scroller').css('position','absolute');
stuck = false;
}
});
});
Update
Switching the #scroller from relative to fixed removes it from the normal flow of the page, this can have unintended consequences for the layout as it re-flows without the missing block. If you change #scroller to use an absolute position it will be removed from the normal flow and will no longer cause these side-effects. I've updated the above example and the linked jsfiddle to reflect the changes to the JS/CSS.
I also changed the way that stickAt is calculated as well, it uses .offset() to find the exact position of the top of #scoller instead of relying on the CSS top value.
Instead of setting the top distance at each scroll event, please consider only switching between a fixed position and an absolute or relative position.All browsers will appreciate and Especially IE.
So you still listen to scroll but you now keep a state flag out of the scroll handler and simply evaluate if it has to switch between display types.
That is so much more optimized and IE likes it.
I can get flickers in Chrome as well if I scroll very quickly. Instead of updating the top position on scroll, instead used the fixed position for your element once the page has scrolled below the threshold. Take a look at the updated fiddle: http://jsfiddle.net/nLK7j/2/
I am trying to re-create website with parallax effect using JavaScript. That means that I have two or more layers, that are moving different speeds while scrolling.
In my case I'm moving only one layer, the other one remains static:
layer 1 = website text;
layer 2 = element background;
for this I'm using simple source code (with jQuery 1.6.4):
var docwindow = $(window);
function newpos(pos, adjust, ratio){
return ((pos - adjust) * ratio) + "px";
}
function move(){
var pos = docwindow.scrollTop();
element.css({'top' : newpos(pos, 0, 0.5)});
}
$(window).scroll(function(){
move();
});
The Problem:
- All calculations are done right and the effect "works" as expected. But there is some performance glitch under some browsers (Chrome MAC/Windows, Opera MAC, IE, paradoxically not Safari).
What do I see during scrolling?
- While scrolling the background moves in one direction together with scroll, but it seems to occasionally jump few pixels back and then forth, which creates very disturbing effect (not fluid).
Solutions that I tried:
- adding a timer to limit scroll events
- using .animate() method with short duration instead of .css() method.
I've also observed, that the animation is smooth when using .scrollTo method (scrollTo jQuery plugin). So I suspect that there is something wrong with firing scroll events (too fast).
Have you observed the same behavior?
Do you know, how to fix it?
Can you post a better solution?
Thanks for all responses
EDIT #1:
Here you can find jsfiddle demonstration (with timer): http://jsfiddle.net/4h9Ye/1/
I think you should be using scrollTop() instead and change the background position to fixed. The problem is that setting it to absolute will make it move by default when you scroll up or down.
The flicker occurs because the position is updated as part of the default browser scroll and updated again as part of your script. This will render both positions instead of just the one you want. With fixed, the background will never move unless you tell it so.
I've created a demo for you at http://jsfiddle.net/4h9Ye/2/ .