实战onscroll事件性能优化
2015年6月19日

在绑定onscroll事件时,我们应当着重关注其性能问题,因为onscroll与其他的鼠标、键盘等事件相比,它被触发的频次很高,间隔很近。如果onscroll事件中涉及到大量的位置计算、元素重绘等工作且这些工作无法在下一个onscroll事件触发前完成,就会造成浏览器掉帧。加之用户鼠标滚动往往是连续的,就会持续触发onscroll事件导致掉帧扩大、浏览器CPU使用率增加、用户体验受到影响。由于开发人员的机器配置普遍都不错,可能同一个onscroll的事件在开发机上只需要10ms,然而在普通配置的机器上需要30ms,因此这类问题常会被开发人员忽视。这篇文章将介绍如何避免onscroll事件潜在的性能问题。

稀释onscroll事件

鼠标滚动时onscroll事件触发的间隔大概只有10~20ms,但我们的眼睛往往没有那么敏感,因此很多情况下其实并不需要如此密集地执行onscroll事件。我们可以通过如下代码来限制onscroll主体方法执行的最短间隔时间。

var scroll = function () {
    // 你想执行的onscroll事件
};
var waiting = false;
$(window).scroll(function () {
    if (waiting) {
        return;
    }
    waiting = true;

    scroll();

    setTimeout(function () {
        waiting = false;
    }, 100);
});

通过设置一个 waiting 变量标识限制 scroll() 方法最快只能每100毫秒执行一次。

上述代码有一个弊端,一次滚动完成后的最后一个 scroll() 方法很有可能恰巧处于 waiting = true 阶段,会造成缺少最后一次 scroll() 方法的执行。如果你的 scroll() 方法必须时时响应滚动条所处位置,那么可以在onscroll事件中额外添加一次200ms的延迟执行来解决这个问题,代码如下:

var scroll = function () {
    // 你想执行的onscroll事件
};
var waiting = false, endScrollHandle;
$(window).scroll(function () {
    if (waiting) {
        return;
    }
    waiting = true;
    
    // 清除之前设置200ms延迟执行
    clearTimeout(endScrollHandle);

    scroll();

    setTimeout(function () {
        waiting = false;
    }, 100);
    
    // 200ms后再次执行一次 scroll() 以防止滚动在下一个100ms等待周期内就结束
    endScrollHandle = setTimeout(function () {
        scroll();
    }, 200);
});

使用requestAnimationFrame

如果你的网站只需要兼容最新的浏览器,那么我强烈建议你使用 requestAnimationFrame 来触发滚动事件。浏览器每一帧都在不断地重绘页面,requestAnimationFrame 方法提供了一个时间点让你在浏览器下一次重绘前执行一个回调方法来更新页面上的元素。这可以同时确保你的滚动事件在一个合适的时间点被触发和页面流畅滚动。

var scroll = function () {
    // 你想执行的onscroll事件
};
var raf = window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    window.oRequestAnimationFrame;
var $window = $(window);
var lastScrollTop = $window.scrollTop();

if (raf) {
    loop();
}

function loop() {
    var scrollTop = $window.scrollTop();
    if (lastScrollTop === scrollTop) {
        raf(loop);
        return;
    } else {
        lastScrollTop = scrollTop;
        
        // 如果进行了垂直滚动,执行scroll方法
        scroll();
        raf(loop);
    }
}

精简onscroll内的操作

虽然使用上述两种方法可以从结构上避免onscroll事件过度消耗资源的问题,但我们还是应该使 scroll() 方法执行尽可能快。一些变量的初始化、不依赖于滚动位置变化的计算等都应当在onscroll事件外提前就绪。

var scroll = function () {
    if ($(window).scrollTop - $('#header').height() > 1000) {
        $('#navigator').css('position', 'fixed');
    }
};
var headerHeight = $('#header').height();
var $navigator = $('#navigator');
var $window = $(window);
var scroll = function () {
    if ($window.scrollTop() - headerHeight > 1000) {
        $navigator.css('position', 'fixed');
    }
};

上面两段代码中第二段代码执行效率比较高,因为把jQuery对象初始化、不变的高度值都缓存在onscroll事件外部了。