I’m trying to implement a simple “fixed headers” table. I know this can in theory be done with CSS only, but it doesn’t work very well when it comes to OSX Lion and its disappearing scrollbars. So I’m doing it with jQuery.
An approach is simple, it’s just 1.5 lines of code:
$('.inbox').scroll(function() {
$(this).find('.inbox-headers').css('top', $(this).scrollTop());
});
Demo.
This works fine and smooth in Firefox, but lags horribly in webkit browsers. Why is that happening and how do I optimise this code? Or maybe approach the problem differently.
The "scroll" event is fired very frequently. It's always going to be really hard to keep up if you're modifying the DOM while scrolling in some browsers.
What you can do is one of these things:
In your case, position: fixed; might be a good alternative.
If not, then have the event handler start a timer for like 100 milliseconds in the future, canceling any previous timer in the process. That way, the DOM will be updated only after scrolling stops or pauses.
If you want continuous updates, keep track of the timestamp when you do an update, and do nothing in the handler if it's been less than some amount of time (100ms or whatever).
function debounce(func, wait) {
var timeout;
return function() {
var context = this,
args = arguments,
later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
$('.inbox').scroll(debounce(function() {
$(this).find('.inbox-headers').css('top', $(this).scrollTop());
}, 100));
This is a little debounce function I use a lot in situations like this.
The best way to do static header is to strictly separate the header and the body of table:
Then you should apply a overflow:scroll style to .body DIV only
No absolute positioning
No scroll events
If you table is very wide then in any case you need use scroll events
Related
This is a general question about a problem I run into often, where I need something to happen at a certain screen width, or scrollTop position, so I end up triggering events for the entire scroll or resize event. This seems really unnecessary and hurts performance. I am wondering what steps I can take to limit calling code written inside scroll or resize events so that I am only triggering these events when I need them.
In the example below I just want the background color to change at a certain scrollTop offset, but since its wrapped in a scroll event, it gets trigged for every pixel.
I know there are things like lodash, but wouldn't I have the same problem of a throttle running just as often on scroll? Any general approach help would be greatly appreciated.
$(window).on('scroll', function() {
var scrollPosition = $(window).scrollTop();
if (scrollPosition > 500) {
$('.container').css('background-color', 'blue');
} else {
$('.container').css('background-color', 'red');
}
});
.container {
background-color: red;
height: 2000px;
width: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
</div>
You should really have a look at Intersection Observer (IO), this was created to solve problems like you described.
With IO you tell the browsers which elements to watch and the browser will then execute a callback function once they come into view (or leave the view) or intersect with each other.
First you have to set the options for your observer:
let options = {
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
Here for example I specified that everytime the observed element is fully visible in viewport I want to execute a callback function. Obviously you can set the parameters to your liking.
Second you have to specify which elements you want to observe:
let target = document.querySelectorAll('.container');
observer.observe(target);
Here I say I want to watch all elements on the page with the class container.
Last I have define the callback function which will be triggered everytime one container element is visible.
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element
});
};
With this approach you don't have to worry about performance issues of scroll events.
Since you can theoretically build all of this with listening to scroll events too you can also use this official polyfill from w3c to support older browsers.
You can also stop observing an element if you don't want to observe it anymore.
Lastly have a look at this demo, it shows you can easily change the background-color of an element depending on how much of the element is visible.
You definitely should use throttle or debounce for scroll or resize handlers. It can be lodash or your own implementation. Cancelled handler triggering costs almost nothing in term of performance, so don't even bother about that.
You can use a library like Waypoint.js to accomplish a similar feature. You'll just need to add an element at the event position and it should work quite efficient. Otherwise there aren't that many other ways except the ones that Huangism already mentioned. As you also asked about resizing events it may be better to use CSS media rules because these are quite performant and easy to use:
#media only screen and (max-width: 600px) {
.container {
background-color: blue;
}
}
As mentioned in comments, you just need to throttle the action. Here is a sample code of how scrolling throttling would work
The expensive part of the scroll/resize event is the part where you are doing something, like when you getting the scrolltop and comparing it then running something. By throttling, your executable code don't actually run until the threshold is reached which saves you big time on performance
Resizing would be the same except you do it for the resize function of course
scrollTimer = setTimeout(function() {
// your execution code goes here
}, 50);
var scrollInit = false;
var scrollTimer = null;
if (!scrollInit) {
var waiting = false;
$(window).on("scroll", function(event) {
if (waiting) {
return false;
}
clearTimeout(scrollTimer);
waiting = true;
scrollTimer = setTimeout(function() {
console.log("scroll event triggered");
}, 50);
});
}
scrollInit = true;
div {
background: grey;
height: 2000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div></div>
I'm waist deep in my own React virtualization implementation and one of the minor issues that has been annoying me is that if I middle click on an item in my list and start scrolling, once that element is removed from the DOM the scrolling halts. My first theory was that the element was gaining focus and that preventing that would solve the issue, but what I've tried hasn't been working and I'm not even sure that's the issue.
How can I prevent this from happening?
See this fiddle for a basic demonstration:
https://jsfiddle.net/v169xkym/2/
And the relevant bit of code that handles virtualization:
$('#container').scroll(function(e) {
$('#container').children().each(function(i) {
if ($('.item:eq(' + i + ')').length > 0) {
if ($('.item:eq(' + i + ')').offset().top < 0) {
$('.item:eq(' + i + ')').remove();
$('#topPadding').height($('#topPadding').height() + 45);
}
}
});
});
Basically, I'm using the standard method of removing the element and upping the padding. In my React implementation this is handled different but here you get a basic functional representation.
you can get around this by not having the disappearing element register mouse events.
this can be done with CSS3 :
div.item {
pointer-events : none;
}
(Not entirely sure why, but my guess is that once the element disappears, the origin of the event is missing, so browsers simply stop doing what they were doing.)
Jsfiddle here
Maybe a bit late to the party. A workaround I am using on a virtual scroller is to detect when there is a scroll event, and when there has been no new events for a time, I consider the scroll is complete.
let scrollTimer = null;
let isScrolling = false;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimer);
isScrolling = true;
scrollTimer = setTimeout(()=>{
isScrolling = false;
},500);
}, false);
I then grab a reference to the element that is hovered at the time isScrolling becomes true (using mouseOver) and prevent this element being unloaded until isScrolling is false. It is a bit of a juggle, but works. I am hoping I can find something simpler as it only seems to be a Chrome problem.
Update: It seems to be a known bug, about to be fixed related to pointer-events: none on something that overlays a virtual scroller (reproduction by someone https://codepen.io/flachware/pen/WNMzKav). I have no idea why my work around above works, but nice to know it wont be needed come Chrome 103. https://bugs.chromium.org/p/chromium/issues/detail?id=1330045&q=chrome%20scroll&can=2&sort=-opened
seen afew websites with this effect, however it seems to drop the framerate in my attempt. I basically want to change the opacity of an element the more the user scrolls.
$(window).scroll(function(event){
$("#responsive-slider-with-blocks-1").css("opacity", 1 - $(window).scrollTop() / 1500);
}
Is there a better way to do this? (would be ace just CSS, but not possible).
I'm really not a fan of binding to the scroll event.
Edit:
Due to changing the opacity on an element which covers the entire viewport could be why the framerate drops so much. Would fading in black div covering the element maybe not drop the framerate so much?
Scroll events fire so fast, you're right, every little optimization will help. The docs for the scroll event have advice along those lines:
Since scroll events can fire at a high rate, the event handler shouldn't execute computationally expensive operations such as DOM modifications. Instead, it is recommended to throttle the event using requestAnimationFrame, setTimeout or customEvent...
You can adapt the example they have there to your purposes (and I'm trying to leave out jquery on purpose to remove the overhead):
var last_known_scroll_position = 0;
var ticking = false;
var responsiveSlider = document.getElementById('responsive-slider-with-blocks-1');
function doSomething(scroll_pos) {
responsiveSlider.style.opacity = 1 - scroll_pos / 1500;
}
window.addEventListener('scroll', function(e) {
last_known_scroll_position = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(last_known_scroll_position);
ticking = false;
});
}
ticking = true;
});
This is certainly longer, and there are some global scope messes to consider, but something like this may make the performance difference you are looking for.
Scroll event I believe will be triggered very often during scrolling. When scroll event triggered, jQuery needs to find DOM element based on the selector. This operation alone is quite expensive.
Changing the opacity make it worse as more pixels had to be processed.
Move code to select DOM using jQuery selector outside scroll event handler. That way you can avoid jQuery to lookup DOM element each time scroll event fires.
Limit size of element to reduce number of pixels need to be compute when opacity changed.
Change opacity at certain time interval helps reduce number of paint operations that browser need to do during scrolling operation. So instead of changing opacity everytime event fires, you wait until certain time has elapsed and then change opacity.
So I have two sections of content near the top of my page and I’d like for users who have scrolled down to near the top of the second section to get “scroll snapped” to the top of the second one once they have stopped scrolling.
I think it should be possible using jQuery but I haven’t been able to figure it out. Here are my examples:
Without my attempt: http://codepen.io/jifarris/pen/gaVgBp
With my broken attempt: http://codepen.io/jifarris/pen/gaVgQp
Basically I can’t figure out how to make it try scrolling to the spot only once, after scrolling has stopped. It’s kind of just freaking out.
I love how the recently introduced scroll snap points CSS feature handles scroll snapping and I’d almost prefer to use it – for the browsers that support it, at least – but it seems like it only works for items that take up 100% of the viewport height or width, and it seems like it’s for scrolling within an element, not the page itself.
The top section has a fixed height, so this really can be handled with pixel numbers.
And for reference, here’s the heart of the code from my attempt:
$(function() {
$(document).on('scroll', function() {
var top = $(document).scrollTop();
if (top > 255 && top < 455) {
$('html, body').animate({scrollTop: '356'}, 500);
$('body').addClass('hotzone');
} else {
$('body').removeClass('hotzone');
}
});
});
KQI's answer contains most of the steps required to create a well functioning section-scroll for use in your application/webpage.
However, if you'd just want to experiment yourself, developing your script further, the first thing you'll have to do is add a timeout handler. Otherwise your logic, and therefor scrollAnimation, will trigger every single pixel scrolled and create a buggy bouncing effect.
I have provided a working example based on your script here:
http://codepen.io/anon/pen/QjepRZ?editors=001
$(function() {
var timeout;
$(document).on('scroll', function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
var top = $(document).scrollTop();
if (top > 255 && top < 455) {
$('body').animate({
scrollTop: '356'
}, 500);
$('body').addClass('hotzone');
} else {
$('body').removeClass('hotzone');
}
}, 50);
});
});
Good luck!
All right, there are couple of things you gonna have to deal with to get a good result: which are performance, call stack queue, easing.
Performance wise you should drop jQuery animate and use VelocityJs which gives a smoother transition, better frame per second (fps) to avoid screen glitches especially on mobiles.
Call stack: you should wrap whatever logic you have to animate the scrolltop with 'debounce' function, set the delay for let say 500mm and check the scrolling behavior. Just so you know, the 'scroll' listener your using is firing on each pixel change and your script will go crazy and erratic. (It is just gonna be a moment of so many calc at the same time. Debounce will fix that for you)
Easing: make the transition looks cool not just dry snappy movement.
Remember, 'easing' with Velocity starts with 'mina.' i.e.
'Mina.easingFnName'
Finally, your logic could be right, i am in my phone now cannot debug it but try to simplify it and work with a single problem at once, be like i.e.
If ( top > 380 ) // debounce(...)
I need to know the current scroll position every ±100ms while the user is scrolling. That position determines which part of the page is "illuminated".
With $(window).on('scroll', function(){}); everything works just fine. I used _.debounce to debounce the event and check in every 100ms where the document is now.
However - on an iPad - 'scroll' isn't triggered until the scrolling has fully stopped, which is terrible in my scenario, so I'm trying to figure out a better solution.
At first, I wanted to use setInterval and check the position every 100ms that way, but I read that it's not as efficient on mobile devices, and it's going to run even if the tab isn't open. So I stumbled on requestAnimationFrame, and at the moment, it looks like I could do this:
saved_pos = -1;
rAF = window.requestAnimationFrame;
set_sticky_pos = function() {
if (saved_pos === window.scrollY) {
rAF(set_sticky_pos);
return false;
}
saved_pos = window.scrollY;
debounced_trigger_function(saved_pos);
rAF(set_sticky_pos);
};
My debounced_trigger_function would then check the current position, and according to it illuminate the content needed by adding a class on it's parent element.
By doing this - is there anything I should be aware of ? Is it a big no-no ?
Note: You may have noticed that I'm not doing any real "animation" which is what ( I think ) rAF was actually designed for, but it seems like the only way to counter the iPad on-scoll slowness. That's exactly why I decided to post the question on SO - is it okay to use rAF even if I'm not "animating" ?
TL;DR
Is it okay to use rAF as a workaround for live scroll position detection, and not for "animating" ?
Since iOS devices are using webkit browsers you can achieve smooth scrolling on them with one single line of CSS:
-webkit-overflow-scrolling: touch;
If that doesn't work for you than you can use rAF with a customized polyfill and/or fallback to scrolling if rAF is not available, but that is quite easy to verify:
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
// defualt scrolling logic or setInterval
window.setTimeout(callback, 1000 / 60);
};
})();
Also, if you don't want to mess with default scrolling on desktop browser you can use Modernizr to detect if the device running your code is touch enabled or not, and than (and only than) you can override the default scrolling behaviour. This would be as easy as
if(Modernizr.touch) {
// your custom scrolling logic here, rAF for example
}