Web page with parallax lags on scroll - javascript

I'm currently developing a web page with a scrolling parallax effect (Stellar.js) on the header and three other sections of the site: however, scrolling them causes lag, especially at the top of the page.
I've already tried to reduce the background images' size by using compression, but it hasn't made too much difference; removing the blur effect didn't solve the problem, either (it did reduce the lag, but it still wasn't smooth enough).
The website runs pretty well on Firefox (W10), with almost no frame drops, but there's quite some lag on Chrome (both Windows and OS X) and Safari.
There's a few JS scroll-triggered scripts running, but I don't know if those may be the cause. Any suggestions?

What you're going to want to do is throttle scroll events. Debouncing events means an event can't fire again until after a certain amount of time. Throttling events means that the event can only fire so much per period of time.
Here's function to throttle events (credit: http://sampsonblog.com/749/simple-throttle-function)
// Create the listener function
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
To use it just do something like this:
function callback () {
console.count("Throttled");
}
window.addEventListener("scroll", throttle( callback, 200 ));

Related

Timer Fired event is pending

I have some code for swipe gesture, the main parts are:
this.topSlide = this.elementRef.nativeElement.querySelector('.product_rate_slide');
if (this.topSlide) {
this.topSlide.addEventListener('touchstart', this.handleTouchStart);
this.topSlide.addEventListener('touchmove', this.handleTouchMove);
this.topSlide.addEventListener('touchend', this.handleTouchEnd);
}
and this is a part of the handler for TouchEnd:
private handleTouchEnd = (evt) => {
if (this.left > 150) {
const rightInterval = setInterval(() => {
this.left += 30;
if (this.left > 500) {
clearInterval(rightInterval);
this.removeTopSlide();
this.addListener();
this.slideSwiped.emit(evt);
}
this.cdr.detectChanges();
}, 17);
Code inside setInterval gets called every 2 seconds (note the interval is set to 17ms)
This works fine on fast machines, the problem occurs when running on a real mobile device (tested with Samsung Galaxy S8) or setting Chrome Performance CPU throttling to 6x slowdown.
The timeout is more a 'request', if the device is too busy doing other stuff, like repainting the DOM and it is not fast enough to keep up, you get delays longer than you want.
So it can be that you need to do something different on slow devices. Besides that: it is better to use setTimeout than setInterval, set a new timeout when the first call is finished. So events don't stack up and get fired at the same time.
Reference (check: Reasons for delays longer than specified):
https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Reasons_for_delays_longer_than_specified

How to interrupt previous event triggers in jQuery

So I've got a scroll event. It does a load of stuff to work out whether something should be moved on the page. When you scroll down, it fires off. If you wheel down, drag, it fires of bazillions and bazillions of times. As you'd expect, perhaps. Here's some simple dummy code to represent the sequence of events.
function scroller() {
// 1. A really expensive calculation that depends on the scroll position
// 2. Another expensive calculation to work out where should be now
// 3. Stop current animations
// 4. Animate an object to new position based on 1 and 2
}
$(window).on('resize' scroller);
Don't get me wrong, it's usually accurate so there isn't so much a concurrency issue. My animations inside the event call .stop() (as part #3) so the latest version is always* the right one but it's eating up a lot of CPU. I'd like to be a responsible developer here, not expecting every user to have a quad core i7.
So to my question... Can I kill off previous calls to my method from a particular event handler? Is there any way I can interfere with this stack of queued/parallel-running "processes" so that when a new one is added to the stack, the old ones are terminated instantly? I'm sure there's a concurrency-minded way of wording this but I can't think of it.
*At least I think that's the case - if the calculations took longer in an earlier run, their animation could be the last one to be called and could cock up the entire run! Hmm. I hadn't thought about that before thinking about it here. Another reason to stop the previous iterations immediately!
You can ensure the event is fired a maximum of once per x milliseconds. E.g.:
(function ($) {
$.fn.delayEvent = function (event, callback, ms) {
var whichjQuery = parseFloat($().jquery, 10)
, bindMethod = whichjQuery > 1.7 ? "on" : "bind"
, timer = 0;
$(this)[bindMethod](event, function (event) {
clearTimeout (timer);
timer = setTimeout($.proxy(callback, this, event), ms);
});
return $(this);
};
})(jQuery);
$(window).delayEvent("resize", scroller, 1000);
Minimalistic demo: http://jsfiddle.net/karim79/z2Qhz/6/

In Javascript, queuing the execution of function if the function is already executing, but cancel any previously queued

I've faced the following scenario quite often so I'm wondering if there is a built-in jQuery way of solving the issue.
Imagine the following code:
$(document).click(function() {
paintCanvas();
});
The problem with this code is that if the user clicks on the screen 50 times in rapid succession you are going to overload the browser with 50 calls to paintCanvas.
If paintCanvas is currently executing and a new request is created, we want to queue the new request so that it waits until paintCanvas is finished executing. However, at the same time, we can drop any previously queued calls to paintCanvas as we only care about the final state of the mouse, not all the intermediate states.
Here is some code that solves the problem:
var _isExecuting, _isQueued;
function paintCanvas() {
if (_isExecuting) {
if (!_isQueued) {
_isQueued = true;
setTimeout(function() {
_isQueued = false;
paintCanvas();
}, 150);
}
return;
}
_isExecuting = true;
// ... code goes here
_isExecuting = false;
};
This AJAX queue plugin essentially implements this functionality, but does so only in terms of AJAX. Surely this is a very common problem that can be solved in more generic way?
You shouldn't have to solve this problem with mousemove because the system already does that for you. While paintCanvas is executing, it is not generating hundreds of mousemove events even if the mouse is moving vigorously. Rather, the next event will be the current location of the mouse, not a queue of all the intervening mouse events.
Look at this jsFiddle: http://jsfiddle.net/jfriend00/4ZuMn/.
Wiggle your mouse around in the body (lower, right pane) as fast as you want. Then move the mouse out of the pane and notice that the count stops immediately - there are no more mouse events. It doesn't stack up mouse events ever. Whenever the system is ready for the next mouse event, it gets the latest position of the mouse. Individual mouse moves are NOT queued up - they do not accumulate. You can also see in the listing of mouse events that lots of intervening mouse events are not present (e.g. lots of coordinates are missing) even though the mouse went through more positions. This is because the system wasn't ready to make a mouse event when the mouse was in that position so that position was skipped.
Further, because javascript is single threaded, you will never get a new mouse event while you are currently processing one. The system won't generate a new one until you're done processing the one you're already one. So, you will never, ever see _isExecuting as true in javascript in your code. You simply don't need that check. And, since you don't need that check and it will never be true, none of your queuing code will ever execute. You can see here in this jsFiddle, that you can never catch a mousemove event that was re-entered: http://jsfiddle.net/jfriend00/ngnUT/. The inAction flag is never caught as true, no matter how fast or much you wiggle your mouse around.
Sounds like you want throttle/debounce features.
There are no built in methods that I know of from jQuery, you can use any of these though:
http://benalman.com/projects/jquery-throttle-debounce-plugin/
http://jsperf.com/jquery-throttle-methods
Though #rkw provided a link, I always prefer to show code here on SO. Here's some simple code that kind does what you want. A function that returns a buffered version of another function. This will keep delaying until it stops receiving the event for the given delay. You can tweak this if you don't want to to wait for the delay after the last event. All you'd need to do is keep track of when you first set the timeout and offset the subsequent calls to setTimeout.
Here's a working example http://jsfiddle.net/mendesjuan/qfFjZ/
function createBuffered(handler, delay) {
var timeoutId = null;
return function() {
var me = this;
if (timeoutId) {
window.clearTimeout(timeoutId);
}
timeoutId = setTimeout(function() {
handle.apply(me, arguments);
timeoutId = null;
}, delay);
}
}

When using setInterval, if I switch tabs in Chrome and go back, the slider goes crazy catching up

I have a jQuery slider on my site and the code going to the next slide is in a function called nextImage. I used setInterval to run my function on a timer, and it does exactly what I want: it runs my slides on a timer. BUT, if I go to the site in Chrome, switch to another tab and return, the slider runs through the slides continuously until it 'catches up'. Does anyone know of a way to fix this. The following is my code.
setInterval(function() {
nextImage();
}, 8000);
How to detect when a tab is focused or not in Chrome with Javascript?
window.addEventListener('focus', function() {
document.title = 'focused';
},false);
window.addEventListener('blur', function() {
document.title = 'not focused';
},false);
To apply to your situation:
var autopager;
function startAutopager() {
autopager = window.setInterval(nextImage, 8000);
}
function stopAutopager() {
window.clearInterval(autopager);
}
window.addEventListener('focus', startAutopager);
window.addEventListener('blur', stopAutopager);
Note that in the latest version of Chromium, there is either a bug or a 'feature' which is making this less reliable, requiring that the user has clicked at least once anywhere in the window. See linked question above for details.
I post an answer here: How can I make setInterval also work when a tab is inactive in Chrome?
Just do this:
setInterval(function() {
$("#your-image-container").stop(true,true);
nextImage();
}, 1000);
inactive browser tabs buffer some of the setInterval or setTimeout functions.
stop(true,true) - will stop all buffered events and execute immadietly only last animation.
The window.setTimeout() method now clamps to send no more than one timeout per second in inactive tabs. In addition, it now clamps nested timeouts to the smallest value allowed by the HTML5 specification: 4 ms (instead of the 10 ms it used to clamp to).
A few ideas comes to mind:
Idea #1
You can make it so that a short burst is idempotent. For example, you could say:
function now() {
return (new Date()).getTime();
}
var autopagerInterval = 8000;
function startAutopager() {
var startImage = getCurrentImageNumber();
var startTime = now();
var autopager = setInterval(
function() {
var timeSinceStart = now() - startTime();
var targetImage = getCurrentImageNumber + Math.ceil(timeSinceStart/autopagerInterval);
if (getCurrentImageNumber() != targetImage)
setImageNumber(targetImage); // trigger animation, etc.
},
autopagerInterval
);
return autopager;
}
This way even if the function runs 1000 times, it will still run in only a few milliseconds and animate only once.
note: If the user leaves the page and comes back, it will have scrolled. This is probably not what the original poster wants, but I leave this solution up since it is sometimes what you want.
Idea #2
Another way to add idempotence (while still keeping your nextImage() function and not having it scroll to the bottom of the page) would be to have the function set a mutex lock which disappears after a second (cleared by another timeout). Thus even if the setInterval function was called 1000 times, only the first instance would run and the others would do nothing.
var locked = false;
var autopager = window.setInterval(function(){
if (!locked) {
locked = true;
window.setTimeout(function(){
locked=false;
}, 1000);
nextImage();
}
}, 8000);
edit: this may not work, see below
Idea #3
I tried the following test:
function f() {
console.log((new Date()) + window.focus());
window.setTimeout(f, 1000);
}
f();
It seems to indicate that the function is being called every second. This is odd... but I think this means that the callbacks are being called, but that the page renderer refuses to update the page in any graphical way while the tab is unfocused, delaying all operations until the user returns, but operations keep piling up.
Also the window.focus() function doesn't say if the window has focus; it GIVES focus to the window, and is thus irrelevant.
What we want is probably this: How to detect when a tab is focused or not in Chrome with Javascript? -- you can unset your interval when the window loses focus (blur), and reset it when it gains focus.
I don't know exactly what is going on in your function nextImage(), but I had a similar issue. I was using animate() with setInterval() on a jQuery image slider that I created, and I was experiencing the same thing as you when I switched to a different tab and back again. In my case the animate() function was being queued, so once the window regained focus the slider would go crazy. To fix this I just stopped the animate() function from queuing.
There are a couple ways you can do this. the easiest is with .stop(), but this issue and ways to fix it are documented in the jQuery docs. Check this page near the bottom under the heading additional notes: http://api.jquery.com/animate/
I had faced similar issue, somehow this code below works fine for me.
var t1= window.setInterval('autoScroll()', 8000);
window.addEventListener('focus', function() {
focused = true;
window.clearInterval(t1);
t1 = window.setInterval('autoScroll()', 8000);
},false);
window.addEventListener('blur', function() {
focused = false;
window.clearInterval(t1);
},false)
function autoScroll()
{
if ( running == true){
if ( focused = true){
forwardSlide();
}
}
else {
running = true;
}
}
If you are using Soh Tanaka's image slider then just add this...to solve your Google Chrome issue:
$(".image_reel").stop(true, true).fadeOut(300).animate({ left: -image_reelPosition}, 500 ).fadeIn(300);
Take note of the .stop() function. Ignore the fading in and out stuff, that's what I used on my version
Thanks
Seconding the comment by jgerstle to use page visibility events instead, see https://www.w3.org/TR/page-visibility/#example-1-visibility-aware-video-playback for more around subscribing to 'visibilitychange' for hidden/visible states.
This seems to be more useful than focus/blur these days as it covers visible-but-not-selected windows if concerned also about multi-window operating systems.

capture multiple "onkeydown" and wait until "onkeyup" to execute

In my web app, I use the onkeydown event to capture key strokes.
For example, I capture the 'j' key to animate a scroll down the page (and do some other stuff meanwhile).
My problem is the user might keep the 'j' key down to scroll further down the page (this is equivalent to fast multiple key strokes).
In my app, this result in a series of animations that doesn't look that good.
How can I know when the key has been released, and know the amount of key stokes I should have captured? This way I could run one long animation instead of multiple short ones.
Building on #JMD:
var animate = false;
function startanimation()
{
animate = true;
runanimation();
}
function stopanimation()
{
animate = false;
}
function runanimation()
{
if ( animation_over )
{
if ( !animate )
{
return;
}
return startanimation();
}
// animation code
var timeout = 25;
setTimeout(function(){runanimation();},timeout);
}
document.onkeydown = startanimation;
document.onkeyup = stopanimation;
You'll need to add some checks for starting/ending animations, however.
Edit: added a return to the JS; would've recursed endlessly.
Rather than trying to stack up the animations, you could start an animation on keyDown, and if at the end of the animation you haven't yet received keyUp then start another animation. As soon as you reach the end of an animation and you do have keyUp then you're done.
setTimeout returns a timer ID. So what I would do is after you receive a keyDown event, I would start a timer with a very short wait period, like so:
var globalTimerId = -1;
var keyDownCount = 0;
function handleKeyDown(e) {
if (globalTimerId != -1) {
clearTimeout(globalTimerId);
keyDownCount++;
}
/* 500 means 1/2 a second, adjust for your needs */
globalTimerId = setTimeout(handleKeyDownAfterWait, 500);
keyDownCount = 1;
}
function handleKeyDownAfterWait() {
globalTimerId = -1;
/* keyDownCount will have the number of times handleKeyDown was called */
}
So the idea is that each time a keyDown event is received, the timer is cleared (assuming it hasn't elapsed yet) and restarted. If the timer expires, the user has let go of the key and you can handle that in handleKeyDownAfterWait.
There may be other more elegant solutions with jQuery or another JS library however. This is just something quick and dirty (and possibly buggy ;))
you can try using my repsonsiveness plugin see:
http://notetodogself.blogspot.com/2008/12/jquery-responsiveness-plugin-for-fast.html
see the demo here:
http://doesthatevencompile.com/current-projects/jquery.responsiveness/
the one where you type stuff fast. you can adapt that to your needs. like so:
the animation event will be bound with the plugin, and execute when the user stops doing something fast. you can count how many times he does the fast thing by normal binding of the event.

Categories

Resources