Jquery scroll event causing performance issues - javascript

I'm trying to use the browser scroll event to place a block of html based on the amount a user has scrolled. The code works but it is causing a huge performance issue which basically forces my browser to freeze.
Any insight as to why and what I could do to resolve this would be greatly appreciated.
<script type="text/javascript">
$('#content').scroll(function () {
var scroll = $('#content').scrollTop();
var $controls = $(".controls").clone();
if (scroll > 200) {
$(".controls").remove();
$('#header').append($controls);
}
else {
$(".controls").remove();
$('.banner').append($controls);
}
});
</script>

First, discovering elements in the DOM is an expensive activity, so cache your jQuery objects.
Second, .append() moves elements around so .clone() and remove() should be unnecessary.
This gives :
var $$ = {//cache of jQuery objects
content: $('#content'),
controls: $(".controls"),
header: $("#header"),
banner: $('.banner')
};
$('#content').scroll(function() {
$controls.appendTo(($$.content.scrollTop() > 200) ? $$.header : $$.banner);
});
Now, you can work on reducing the frequency at which the handler is called, which can be achieved as follows :
var $$ = {//cache of jQuery objects
content: $('#content'),
controls: $(".controls"),
header: $("#header"),
banner: $('.banner')
};
var scrollHandling = {
allow: true,
reallow: function() {
scrollHandling.allow = true;
},
delay: 50 //(milliseconds) adjust to the highest acceptable value
};
$('#content').scroll(function() {
if(scrollHandling.allow) {
$controls.appendTo(($$.content.scrollTop() > 200) ? $$.header : $$.banner);
scrollHandling.allow = false;
setTimeout(scrollHandling.reallow, scrollHandling.delay);
}
});

The scroll function is called for every movement of the scrollbar. That can potentially be a lot of times, so you need to be careful how much code you are running and certainly how much manipulation of the DOM you are doing.
In this case, you'll be repeating a lot of the same actions (clone, append, remove) as the scrolling is occurring, but it appears that you only want to flip between two states as you cross back and forth over that 200 scroll value. You could potentially solve most of the performance issues with something like this:
<script type="text/javascript">
var thresholdCrossed = false;
$('#content').scroll(function () {
var scroll = $('#content').scrollTop();
var threshold = 200;
if (scroll > threshold && !thresholdCrossed) {
var controls = $(".controls").clone();
$(".controls").remove();
$('#header').append(controls);
} else if (scroll <= threshold && thresholdCrossed) {
var controls = $(".controls").clone();
$(".controls").remove();
$('.banner').append(controls);
}
thresholdCrossed = scroll > threshold;
});
</script>
You can do additional work that some of the other answers describe to help reduce wasted resources, but this should give you a general idea of how to hopefully help the main performance concern of constantly modifying the DOM as you scroll. I would probably suggest some combination of this along with the answer provided by #Kolink so that you are truly limiting the DOM manipulation to the least amount necessary.

You are cloning all .controls elements every tick of scrolling, even when it is not needed.
I would suggest cloning the controls on ready and setting it to display:none. Then, just toggle the display based on the scroll position.
On re-reading your question, it looks like you're just moving the controls element from the header to the banner? In that case, you don't even need a clone. However I strongly suggest adding id="controls" to the controls element, and id="banner" - use IDs instead of classes in general if there is only one.
document.getElementById('content').onscroll = function() {
document.getElementById(this.scrollTop > 200 ? "banner" : "header")
.appendChild(document.getElementById('controls'));
};

Every time the scroll bar moves jQuery has to go into the DOM to get the variables you references. To start cache the variables so jQuery doesnt have to do twice the work.
var content = $('#content');
content.scroll(function(){
});

Related

Hashchange history breaks iScroll

I have created the following application using iScroll: http://preview.na-software.co.uk/Demo/FutureLearning4/#/section-0
As the user flicks left and right or clicks the arrows in the bottom corners, the application moves the content sections it updates the history by changing the hash so that the user can move back and forth to other sections and bookmark them etc.
However! If you access a hash like: http://preview.na-software.co.uk/Demo/FutureLearning4/#/section-2 and then navigate a few sections and then use the back buttons two issues happen:
1.) It scrolls to the first screen (even though currentSection is correct, and iScroll has been told the correct section).
2.) If you click the back or forward button multiple times, you stop the animation and cause it to become confused and stick in between two sections.
Looking into the code, and seeing that the correct indexes and elements are being passed to iScroll on hashchange, and console logging out the offsets, I've discovered the issue is cause because the offsets are incorrectly set... however just doing refresh() won't fix the issue, as it will then reset the position.
Can anyone see where the problem is or see a way to fix this?
I should note that this bug ONLY happens if you come into the application on a URL that isn't section 0 and then scroll around the application. This is because the offsets will be created correctly by your interactions. But if you come into a URL like section 3, then the offsets will be incorrect and so the hashchanges don't work correctly, if that makes sense.
The hashchange method looks like:
// handle hashchange events
$(window).hashchange( function(){
// read the hash to find out what the new section number is
var nums = location.href.match(/(section)-\d+/g).map(
function(x){ return +x.replace(/\D/g,"") }
);
// set currentSection
currentSection = nums[0];
// if the hashchange was called by user scrolling
if(hashCalledByScroll){
// no need to anything as they have already updated hash and scrolled
hashCalledByScroll = false;
} else {
// find the section to scrollTo
sectionToScrollTo = $('#horizontal > .sections > .section').eq(currentSection).attr('id');
// tell iscroll to scroll to the section
horizontal.scrollToElement( '#' + sectionToScrollTo, null, null, true );
}
// hide the menu on hashchange
hideMenu();
});
Testing your site, I noticed the following: Whenever I access the site via section-3 and then enter the url for section-2, the navigation would instead send me to section-0.
I believe this is the same behaviour as you are experiencing in 1).
So I investigated and came to the following analysis:
In the function horizontal.scrollToElement( '#' + sectionToScrollTo, null, null, true )
iScroll retrieves the utils.offset(el) [iScroll.js#772] for the given el-ement. This offset tells it, where the element to scroll to is.
iScroll goes through the element and all of its offsetParents to add up their offsets. This is where things are breaking: <div class="sections"> has a negative offset to its parent, which imho it should not have.
This, in turn, messes up the scrollTo-coordinates.
To see what I am talking about: document.querySelector('.sections').offsetLeft
This has all just been analysis. My approach to fix this would be to avoid scrollToElement() and instead use scrollTo():
...
} else {
// find the section to scrollTo
sectionToScrollTo = $('#horizontal > .sections > .section').eq(currentSection).attr('id');
// tell iscroll to scroll to the section
var posLeft = -$('#' + sectionToScrollTo)[0].offsetLeft;
var posTop = -$('#' + sectionToScrollTo)[0].offsetTop;
horizontal.scrollTo(posLeft, posTop, 1000);
}
// hide the menu on hashchange
hideMenu();
});
Thus, just calculate the location of the section you want to go to yourself.
About 2) I am not sure if there is much one can do about it. Jumping around quickly breaks a lot of carousels. Maybe a delayed callback to scrollEnd, verifying the validity of the current state.
Another thing I noticed is that you can accidentally stop the transition. Try to click, hold and release the cursor midway a transition - you need to be quick.
Hope this helps.
Found not best solution and it doesn't solve main problem, but it works.
$(window).hashchange(function () {
if (hashCalledByScroll) {
hashCalledByScroll = false;
} else {
var hpage = window.location.hash;
var hpage = hpage.replace('#/section-', ''); //get number of target page
var cpage = currentSection; //number of current page
var count = parseInt(hpage) - parseInt(cpage); //difference
while (count > 0) { //if difference positive: go forward count-times
horizontal.next();
count--;
}
while (count < 0) { //if difference negative: go backward count-times
horizontal.prev();
count++;
}
}
hideMenu();
});
FIDDLE

Skrollr run animation once only then stop

How can I run an animation in Skrollr once, and then kill it?
I've tried:
The beforerender method but that kills all animations
Looking for ways to set individual Skrollr instances, but it's a singleton
Removing or renaming the object's attributes
nullifying the object's className
Toggling a CSS class
Each time, it looks like the values are cached within Skrollr and fire regardless of my efforts. What am I missing here?
I want to improve this question:
https://github.com/Prinzhorn/skrollr/wiki/Tips
In the wiki under Tips tab, there is a snippet that would be useful:
skrollr.init({
beforerender: function(data) {
return data.curTop > data.lastTop;
}
});
In fact, with this code, it will animate only if you scroll down, so it will result that the animation will be played once, or in other words, it wouldn't "turn back" when you scroll up, acting like the CSS animation-fill-mode: forwards;
Well, I finally found a semi-solution. I've removed the node entirely and re-inserted a new one. Here's my code so you can get the idea:
// Before this, poll window.onscroll to wait for the skrollable-after
// class to be applied.
if (!this.flags.learnmore && Toolbox.hasClass(this.dom.learnmoreslider, 'skrollable-after')) {
// Sort of a misleading name, sorry, not an exact clone
var clone = document.createElement("span");
clone.innerHTML = this.dom.learnmoreslider.innerHTML;
// This was the end condition of the animation
// (it slid text horizontally) so it has to look the same.
clone.style.marginLeft = "250px";
var parent = this.dom.learnmoreslider.parentNode;
parent.appendChild(clone);
// This must be removed after in order to maintain the parent reference.
parent.removeChild(this.dom.learnmoreslider);
// Raise a flag or remove the listener or something, you're done
this.flags.learnmore = true;
}
}
A tad clunky but does the trick. I'm definitely still interested in the correct way to kill an animation.
Something like this should work:
skroller = skrollr.init({
render: function(data) {
scroll = data.curTop;
if ((scroll >= 200) && (!element.hasClass('active'))) {
element.addClass('active').removeAttr('data-start data-end');
skroller.refresh();
}
}
});

Menu items - onclick FadeInDown and FadeOutUp

I have a menu composed of three options.
Clicking on one causes a container div to "FadeInDown".
Then, its contents "FadeIn".
Clicking on another menu item or anywhere else on the page causes the
contents to "FadeOut" then container div to "FadeOutUp".
Here is the fiddle that I have been testing jsfiddle
$(document).ready(function() {
$('.container').each(function() {
animationHover(this,'.fadeInDown');
});
});
I'm not very familiar with jQuery and have been trying to use animate-css to get me along. Thanks for any help and tips in advance and welcome coding criticism :)
My answer is mainly based on JQuery and its animate function (http://api.jquery.com/animate/) . Here is the fiddle :
http://jsfiddle.net/awmat/7/
I use JavaScript objects like fadeInDown to animate the container.
var fadeInDown = {
opacity:1,
top: "50px"
};
And i use the complete callback function of animate to make the content appear after the container.
To manage several div (one for each menu item), I use id as selectors, but since the "click and display" function remains the same, I used a "builder" : (this uses a closure, so if you're not familiar with JavaScript, you may have to read several times to understand what is going on)
var menuClickCallbackBuilder = function(menuItem){
var container = $('#container' + menuItem);
var content = container.find('.content');
var showContent = function(){
content.animate({opacity:1},{duration:1000});
};
return function() {
var activeContainer = $('.active');
var hideContainer = function(){
activeContainer.animate(fadeOutUp,1000);
};
activeContainer.find('.content').animate({opacity:0},{duration: 1000, complete : hideContainer});
activeContainer.removeClass("active");
if(activeContainer[0] != container[0])
{
var timeout = activeContainer[0] ? 2000 : 0 ;
setTimeout(function(){
container.animate(fadeInDown,{duration : 1000, complete :showContent});
},timeout);
container.addClass("active");
}
}
};
This way, when you add the add the click callbacks, you can just do :
$(document).ready(function(){
// note that menuClickCallbackBuilder(1) returns a function
// again if you're not familiar with JS, you may have to re-read menuClickCallbackBuilder
$('#menuLink1').on('click', menuClickCallbackBuilder(1));
$('#menuLink2').on('click', menuClickCallbackBuilder(2));
$('#menuLink3').on('click', menuClickCallbackBuilder(3));
});
Some improvements you can bring to this :
Factor the durations into a variable (e.g animationDurationInSeconds) so that if you want to change the speed of the animation, you only have 1 thing to change. (#Huangism: and right after you did that, make animation faster so that it gets more dynamic)
(From #Huangism) : stop it from going crazy when people cicks on the menu 10 times really fast
Actually, I think you don't need 3 different containers, you could do it with just one container (though I don't know if it would be considered an improvement)
There is probably a way to use CSS classes instead of fadeInDown and fadeOutUp JS objects. That would be cleaner, I think you should keep styles in CSS as much as you can.
There is no need for different IDs for menu items, you could do the exact same thing with a loop.
Whatever your imagination wants to add

Pass mousewheel event through fixed content

The best way to understand this is to look at this fiddle.
Notice how mouse wheel over the fixed content in the red box does nothing. I would like the scrollable div to scroll.
In case the fiddle dies - basically I have a scrollable div with a fixed element over it. Typically when you mouse wheel over a scrollable div it will of course scroll. But if you are over the fixed element instead then no scroll happens. Depending on your site layout this could be counter intuitive to a user.
jQuery solutions are okay.
A much, MUCH simpler, but much less widely supported, answer is the following:
#fixed{ pointer-events:none; }
jsFiddle
Doesn't work in IE at all though unfortunately! But you could use modernizr or somesuch to detect whether it was supported and use the jQuery as a stop-gap where it isn't.
Courtesy of Mr. Dominic Stubbs
I had this problem and this works for me (using jquery):
$(document).ready( function (){
$('#fixed').on('mousewheel',function(event) {
var scroll = $('#container').scrollTop();
$('#container').scrollTop(scroll - event.originalEvent.wheelDeltaY);
return true;
});
});
Works on Safari and Chrome: http://jsfiddle.net/5bwWe/36/
I think this does what you're asking for!
$('#fixed').bind('mousewheel', function(e){
var scrollTo= (e.wheelDelta*-1) + $('#container').scrollTop();
$("#container").scrollTop(scrollTo);
});
EDIT: Updated the jsFiddle link to one that actually works
DOUBLE EDIT: Best to dispense with the .animate() on further testing...
jsFiddle Example
TRIPLE EDIT:
Much less pretty (and will probably be horribly slow with a lot of elements on the page), but this works and I owe a lot to this stackoverflow answer.
$('#fixed').bind('mousewheel', function(e) {
var potentialScrollElements = findIntersectors($('#fixed'), $('*:not(#fixed,body,html)'));
$.each(potentialScrollElements, function(index, Element) {
var hasVerticalScrollbar = $(Element)[0].scrollHeight > $(Element)[0].clientHeight;
if (hasVerticalScrollbar) {
var scrollTo = (e.wheelDelta * -1) + $(Element).scrollTop();
$(Element).scrollTop(scrollTo);
}
});
});
function findIntersectors(targetSelector, intersectorsSelector) {
var intersectors = [];
var $target = $(targetSelector);
var tAxis = $target.offset();
var t_x = [tAxis.left, tAxis.left + $target.outerWidth()];
var t_y = [tAxis.top, tAxis.top + $target.outerHeight()];
$(intersectorsSelector).each(function() {
var $this = $(this);
var thisPos = $this.offset();
var i_x = [thisPos.left, thisPos.left + $this.outerWidth()]
var i_y = [thisPos.top, thisPos.top + $this.outerHeight()];
if (t_x[0] < i_x[1] && t_x[1] > i_x[0] && t_y[0] < i_y[1] && t_y[1] > i_y[0]) {
intersectors.push($this);
}
});
return intersectors;
}
UPDATE (August 2016): It seems the browser implementations have changed and it's no longer possible to re-dispatch a WheelEvent on a different target. See the discussion here.
For an alternative solution that should work across platforms, try this:
var target = $('#container').get(0);
$('#fixed').on('wheel', function (e) {
var o = e.originalEvent;
target.scrollTop += o.deltaY;
target.scrollLeft += o.deltaX;
});
Working example: https://gist.run/?id=6a8830cb3b0564e7b16a4f31a9405386
Original answer below:
Actually, the best way to do it is to copy the original event. I've tried #Tuokakouan's code but scrolling behaves strangely (too fast) when we use a multitouch touchpad that has inertia.
Here's my code:
var target = $('#container').get(0);
$('#fixed').on('wheel', function(e){
var newEvent = new WheelEvent(e.originalEvent.type, e.originalEvent);
target.dispatchEvent(newEvent);
});
You can try it here: http://jsfiddle.net/NIXin/t2expL6u/1/
What I'm trying to do now is also to pass the touch events, without much success. Since mobile phones and touch screens are now more popular, some people might want to scroll using their fingers instead - neither of the answers offered solves that.
Well,all solutions with js are kind of delayed when scrolling on it. if the fixed element you use is just for display, then I have a good css tricks to achieve that.
make the fixed element z-index:-1 and the container element background-color:transparent
here is the jsfiddle you can see: https://jsfiddle.net/LeeConan/4xz0vcgf/1/

How to use jQuery jScrollPane and scrollTo plugins in the same script

I'm building my first js/jQuery site and I've run into a hiccup. I'm trying to use both jScrollpane (Kelvin Luck) and scrollTo (Ariel Flesler) plugins in one script. If I comment one out, the other works. Are they mutually exclusive? Do I need to unbind functionality out of jScrollpane to remove a 'scrollTo' call conflict or something? (I have no idea how to do that).
I'm using jScrollPane 2beta11 and scrollTo 1.4.2. Here's my stripped-down code using both:
// JavaScript Document
$(document).ready(function() {
//jScrollPane Init
$('#scrollingDiv').jScrollPane({
});
//scrollTo Refresh
$('div.scroll-pane').scrollTo( 0 );
$.scrollTo( 0 );
//Buttons
var $scrollDiv = $('#scrollingDiv');
var next = 1;
$('#but-rt').click(function(){
$scrollDiv.stop().scrollTo( 'li:eq(1)', 800 );
next = next + 1;
});
});
I'm aware that jScrollPane has it's own scrollTo functionality, but I need scrollTo's jQuery Object selectors in my particular project. I know I've got my HTML/CSS lined up fine because each function works as long as the other is commented out.
(By the way, I plan on using "next" variable to increment scrollTo button once I figure out how... not related to my problem tho.)
Any help is much appreciated. Let me know if there's anything else I need to supply. Thanks!
-Patrick
See how to use ScrollTo functionality of JscrollPane from the following url,
http://jscrollpane.kelvinluck.com/scroll_to.html
Hope this will help you...
I too was trying to use both jScrollpane (Kelvin Luck) and scrollTo (Ariel Flesler) plugins in one script. I've come across an easy solution which doesn't even require Ariel Flesler's AWESOME Script, if you don't necessarily require animated scrolling.
I wanted to be able to scroll to a label in a list of items when the page loads.
Here's how i did it:
$(function()
//Declare the ID or ClassName of the Scroll Element
//and the ID or ClassName of the label to scroll to
MyList = $('#MyElementID OR .MyElementClassName');
MyLabel = $('#MyElementID OR .MyElementClassName');
// Initiate the Scrollpane
MyScroll = $(MyList).jScrollPane();
// Connect to the jScrollPaneAPI
jScrollPaneAPI = MyScroll.data('jsp');
// Get position co-ordinates of the Label
var MyLabelPosition = $(MyLabel).position();
// Convert position co-ordinates to an Integer
MyLabelPosition = Math.abs(MyLabelPosition.top);
// Scroll to the Label (0-x, vertical scrolling) :)
jScrollPaneAPI.scrollTo(0, MyLabelPosition-3, true);
});
There's a small bug with the exact positioning when a list gets longer,
will post a fix asap...
They are mutually exclusive because jScrollPane removes the real scrolling and replaces it with complex boxes-in-boxes being moved relative to each other via JS.
This is how I successfully mixed them -- I had a horizontal list of thumbnails; this code scrolled the thumbnails to the center:
Activated jScrollPane:
specialScrolling = $('#scrollingpart').jScrollPane();
In my serialScroll code, where I usually would call
$('#scrollingpart').trigger('goto', [pos]);
in my case, inside my
onBefore:function(e, elem, $pane, $items, pos)
I put code like this:
jScrollPaneAPI = specialScrolling.data('jsp');
//get the api to manipulate the special scrolling are
scrollpos=(Math.abs(parseInt($('.jspPane').css('left'), 10)));
//get where we are currently scrolled -- since this is a negative number,
//get the absolute value
var position = $('#scrollingpart .oneitem').eq(pos).position();
//get the relative offset location of the item we are targetting --
//note "pos" which is the index number for the items that you can access
//in serialScroll's onBefore:function
itempos=Math.abs(position.left);
//get just the x-axis location -- your layout might be different
jScrollPaneAPI.scrollBy(itempos-scrollpos-480, 0, true);
//the 480 worked for my layout; the key is to subtract the 2 values as above
Hope this helps someone out there!
This doesn't cater for all use cases (it only handles scrollToY and scrollToElement), but offers a consistent API so you can just use $( /* ... */ ).scrollTo( /* number or selector */ ) and it will work on any element, jScrollPane or native.
You could extend the method condition to cater for all the other jScrollPane methods by inferring the value passed in target though.
(function scrollPaneScrollTo(){
// Save the original scrollTo function
var $defaultScrollTo = $.fn.scrollTo;
// Replace it with a wrapper which detects whether the element
// is an instance of jScrollPane or not
$.fn.scrollTo = function $scrollToWrapper( target ) {
var $element = $( this ),
jscroll = $element.data( 'jsp' ),
args = [].slice.call( arguments, 0 ),
method = typeof target === 'number' ? 'scrollToY' : 'scrollToElement';
if ( jscroll ) {
return jscroll[ method ].call( $element, target, true );
}
else {
return $defaultScrollTo.apply( $element, args );
}
};
}();

Categories