Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detecting if a user has scrolled away from the top

The obvious answer to this is to simply attach an event to the scroll event:

var scrolled = false;

$(window).scroll(function() {
   if($(window).scrollTop() > 0) {
       scrolled = true;
   } else {
       scrolled = false;
   }
});

However, creator of jQuery, John Resig's blogpost from 2011 states: It’s a very, very, bad idea to attach handlers to the window scroll event.

And recommends the following:

var didScroll = false;

$(window).scroll(function() {
    didScroll = true;
});

setInterval(function() {
    if ( didScroll ) {
        didScroll = false;
        // Check your page position
    }
}, 250);

Has anything changed in the past five years? Is John Resig's solution still the best?

like image 303
Chuck Le Butt Avatar asked Oct 21 '15 10:10

Chuck Le Butt


People also ask

How do you check if a user has scrolled to the bottom?

If you want to check whether the user has scrolled to the bottom of the page, you can use the scroll() jQuery event. The given code piece takes the top scroll of the window, so how much the page is scrolled down, it adds the height of the visible window and checks if it is equivalent to the height of the document.

How do I know if my scroll is at the top?

You can check if window. scrollY (the number of pixels the window has scrolled vertically) is equal to 0 . If you want to check if the window has been scrolled to its leftermost, you can check if window. scrollX (the number of pixels the window has scrolled horizontally) is equal to 0 .

How can you tell if someone is at the top of the page?

Use jQuery to Detect if User has Scrolled to the Bottom or Top of a Page. $(window). scrollTop() – returns the current vertical position of the scroll bar.

How do I know if my browser window is scrolled to the bottom?

innerHeight + window. pageYOffset) >= document. body. offsetHeight) { alert("you're at the bottom of the page"); } };


2 Answers

I'd hate to argue with the creator of jQuery but having a brilliant mind doesn't necessarily mean you're always right of course. Using a delay on scroll might ruin any animation based on the event. I've spent a ridiculous amount of time investigating the scroll event and making plugins and demos based on it and it seems somewhat of an urban myth that it triggers very often of most devices.

Here's a small demo to test the amount - the maximum seems to be the display refresh rate :

Codepen

Unless the browser has smooth scrolling enabled, mousewheeling or panning will only result in a single event getting triggered. And even if that's not the case, most machines are very well equipped to handle as many calls as the frame rate will allow - provided the rest of the web page is built well.

That last bit is the biggest issue, browsers can handle quite a bit of script at small intervals but what could slow down execution most is badly written markup. The biggest bottlenecks are therefore often repaints. This can be well inspected when using developer tools.

Some examples of that are having big blocks of fixed position elements and animations that aren't based on transform but use pixel values instead. Both trigger unnecessary repaints which have a huge impact on performance. These problems aren't directly related to how often scroll fires.

Using fixed position elements, repaints on webkit related browsers can be prevented with this hack :

.fixed {
  -webkit-backface-visibility: hidden;
}

This creates a separate stacking order, making the content independent of it's surroundings and preventing the complete page from being repainted. Firefox seems to do this by default but unfortunately I haven't found a property yet that will work with IE so it's best to avoid these kinds of elements in any case, especially if their size is a large portion of the viewport.

The following was not yet implemented at the time of writing of the article mentioned.

And that would be requestAnimationFrame which now has very good browser support. With this, functions can be executed in a way that's not forced on the browser. So if there are complex functions inside the scroll handler, it would be a very good idea to use this.

Another optimisation is to use flagging and not run anything unless it needs to. A helpful tool in this can be to add classes and check for their presence. Here's the approach I generally use :

$(function() {

var flag, modern = window.requestAnimationFrame;

$(window).scroll(function() {

    if (!$(this).scrollTop()) {

        if (flag) {
        if (modern) requestAnimationFrame(doSomething);
        else doSomething();
        flag = false;
        }
    }
    else if (!flag) {

        if (modern) requestAnimationFrame(doMore);
        else doMore();
        flag = true;
    }
});

function doSomething() {

    // special magic
}

function doMore() {

    // other shenanigans
}
});

And an example of how toggling a class could be used to determine a boolean :

$(function() {

var modern = window.requestAnimationFrame;

$(window).scroll(function() {

    var flag = $('#element').hasClass('myclass');

    if (!$(this).scrollTop()) {

        if (flag) {
        if (modern) requestAnimationFrame(doSomething);
        else doSomething();
        $('#element').removeClass('myclass');
        }
    }
    else if (!flag) {

        if (modern) requestAnimationFrame(doMore);
        else doMore();
        $('#element').addClass('myclass');
    }
});

function doSomething() {

    // special magic
}

function doMore() {

    // other shenanigans
}
});

If it doesn't interfere with any desired functionality, debouncing the event can be a good approach of course. It can be as simple as :

$(function() {

var doit, modern = window.requestAnimationFrame;

$(window).scroll(function() {

    clearTimeout(doit);

    doit = setTimeout(function() {

        if (modern) requestAnimationFrame(doSomething);
        else doSomething();

    }, 50);
});

function doSomething() {

    // stuff going on
}
});

Below in the comments, Kaiido had a few valid points. Apart from having to raise the scope of the variable that defines the timeout, this approach may not be too useful in practice because it will only execute a function after scrolling has finished. I'd like to refer to this interesting article, leading to the conclusion that what would be more effective than debouncing here is throttling - making sure the events only run the function a maximum amount per time unit, closer to what was proposed in the question. Ben Alman made a nice little plugin for this (note it was written in 2010 already).

Example

I've managed to extract only the part needed for throttling, it can still be used in a similar way :

$(window).scroll($.restrain(50, someFunction));

Where the first argument is the amount of time within which only a single event will execute the second parameter - the callback function. Here's the underlying code :

(function(window, undefined) {

var $ = window.jQuery;

$.restrain = function(delay, callback) {

    var executed = 0;

    function moduleWrap() {

        var elapsed = Date.now()-executed;

        function runIt() {

            executed = Date.now();
            callback.apply(this, arguments);
        }

        if (elapsed > delay) runIt();
    }

    return moduleWrap;
};
})(this);

What will happen inside the function to be executed can still slow down the process of course. One example of what should always be avoided is using .css() to set style and placing any calculations inside it. This is quite detrimental to performance.

Has to be mentioned as well that the flagging code makes more sense when toggling at a certain point down the page instead of at the top because that position won't fire multiple times. So the check on the flag itself there could be left out in the scope of the question.

Doing this right, it would make sense to also check the scroll position on page load and toggle on the basis of that. Opera is particularly stubborn with this - it resumes the cached position after page load. So a small timeout is best suited :

$(window).on('load', function() {

    setTimeout(function() {

        // check scroll position and set flag

    }, 20);
});

In general I'd say - don't avoid using the scroll event, it can provide very nice effects on a web page. Just be vigilant of how it all sticks together.

like image 130
Shikkediel Avatar answered Sep 19 '22 14:09

Shikkediel


Here solution that I use:

function bindScrollObserverEvent() {
    var self = this;
    $.fn.scrollStopped = function(callback) {
        var $this =$(this),
            self = this;
        $this.scroll(function() {
            if ($this.data('scrollTimeout')) {
                clearTimeout($this.data('scrollTimeout'));
            }
            $this.data('scrollTimeout', setTimeout(callback, 250, self));
        });
    };

        $(window).scrollStopped(function() {
          //You code
         });
like image 44
AlexBerd Avatar answered Sep 22 '22 14:09

AlexBerd