
滚动优化的由来滚动优化其实也不仅仅指滚动scroll 事件还包括了例如 resize 这类会频繁触发的事件。简单的看看1234vari 0;window.addEventListener(scroll,function(){console.log(i);},false);输出如下在绑定 scroll 、resize 这类事件时当它发生时它被触发的频次非常高间隔很近。如果事件中涉及到大量的位置计算、DOM 操作、元素重绘等工作且这些工作无法在下一个 scroll 事件触发前完成就会造成浏览器掉帧。加之用户鼠标滚动往往是连续的就会持续触发 scroll 事件导致掉帧扩大、浏览器 CPU 使用率增加、用户体验受到影响。在滚动事件中绑定回调应用场景也非常多在图片的懒加载、下滑自动加载数据、侧边浮动导航栏等中有着广泛的应用。当用户浏览网页时拥有平滑滚动经常是被忽视但却是用户体验中至关重要的部分。当滚动表现正常时用户就会感觉应用十分流畅令人愉悦反之笨重不自然卡顿的滚动则会给用户带来极大不舒爽的感觉。滚动与页面渲染的关系为什么滚动事件需要去优化因为它影响了性能。那它影响了什么性能呢额......这个就要从页面性能问题由什么决定说起。我觉得搞技术一定要追本溯源不要看到别人一篇文章说滚动事件会导致卡顿并说了一堆解决方案优化技巧就如获至宝奉为圭臬我们需要的不是拿来主义而是批判主义多去源头看看。从问题出发一步一步寻找到最后就很容易找到问题的症结所在只有这样得出的解决方法才容易记住。说教了一堆废话不喜欢的直接忽略哈回到正题要找到优化的入口就要知道问题出在哪里对于页面优化而言那么我们就要知道页面的渲染原理浏览器渲染原理我在我上一篇文章里也要详细的讲到不过更多的是从动画渲染的角度去讲的【Web动画】CSS3 3D 行星运转 浏览器渲染原理 。想了想还是再简单的描述下我发现每次 review 这些知识点都有新的收获这次换一张图以 chrome 为例子一个 Web 页面的展示简单来说可以认为经历了以下下几个步骤JavaScript一般来说我们会使用 JavaScript 来实现一些视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。Style计算样式这个过程是根据 CSS 选择器对每个 DOM 元素匹配对应的 CSS 样式。这一步结束之后就确定了每个 DOM 元素上该应用什么 CSS 样式规则。Layout布局上一步确定了每个 DOM 元素的样式规则这一步就是具体计算每个 DOM 元素最终在屏幕上显示的大小和位置。web 页面中元素的布局是相对的因此一个元素的布局发生变化会联动地引发其他元素的布局发生变化。比如body 元素的宽度的变化会影响其子元素的宽度其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说布局过程是经常发生的。Paint绘制本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等也就是一个 DOM 元素所有的可视效果。一般来说这个绘制过程是在多个层上完成的。Composite渲染层合并由上一步可知对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后浏览器会将所有层按照合理的顺序合并成一个图层然后显示在屏幕上。对于有位置重叠的元素的页面这个过程尤其重要因为一旦图层的合并顺序出错将会导致元素显示异常。这里又涉及了层GraphicsLayer的概念GraphicsLayer 层是作为纹理(texture)上传给 GPU 的现在经常能看到说 GPU 硬件加速就和所谓的层的概念密切相关。但是和本文的滚动优化相关性不大有兴趣深入了解的可以自行 google 更多。简单来说网页生成的时候至少会渲染LayoutPaint一次。用户访问的过程中还会不断重新的重排reflow和重绘repaint。其中用户 scroll 和 resize 行为即是滑动页面和改变窗口大小会导致页面不断的重新渲染。当你滚动页面时浏览器可能会需要绘制这些层(有时也被称为合成层)里的一些像素。通过元素分组当某个层的内容改变时我们只需要更新该层的结构并仅仅重绘和栅格化渲染层结构里变化的那一部分而无需完全重绘。显然如果当你滚动时像视差网站(戳我看看)这样有东西在移动时有可能在多层导致大面积的内容调整这会导致大量的绘制工作。防抖Debouncing和节流Throttlingscroll 事件本身会触发页面的重新渲染同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作例如 DOM 操作就不应该放在事件处理中。针对此类高频度触发事件问题例如页面 scroll 屏幕 resize监听用户输入等下面介绍两种常用的解决方法防抖和节流。防抖Debouncing防抖技术即是可以把多个顺序地调用合并成一次也就是在一定时间内规定事件被触发的次数。通俗一点来说看看下面这个简化的例子123456789101112131415161718192021// 简单的防抖动函数functiondebounce(func, wait, immediate) {// 定时器变量vartimeout;returnfunction() {// 每次触发 scroll handler 时先清除定时器clearTimeout(timeout);// 指定 xx ms 后触发真正想进行的操作 handlertimeout setTimeout(func, wait);};};// 实际想绑定在 scroll 事件上的 handlerfunctionrealFunc(){console.log(Success);}// 采用了防抖动window.addEventListener(scroll,debounce(realFunc,500));// 没采用防抖动window.addEventListener(scroll,realFunc);上面简单的防抖的例子可以拿到浏览器下试一下大概功能就是如果 500ms 内没有连续触发两次 scroll 事件那么才会触发我们真正想在 scroll 事件中触发的函数。上面的示例可以更好的封装一下12345678910111213141516171819202122// 防抖动函数functiondebounce(func, wait, immediate) {vartimeout;returnfunction() {varcontext this, args arguments;varlater function() {timeout null;if(!immediate) func.apply(context, args);};varcallNow immediate !timeout;clearTimeout(timeout);timeout setTimeout(later, wait);if(callNow) func.apply(context, args);};};varmyEfficientFn debounce(function() {// 滚动中的真正的操作}, 250);// 绑定监听window.addEventListener(resize, myEfficientFn);节流Throttling防抖函数确实不错但是也存在问题譬如图片的懒加载我希望在下滑过程中图片不断的被加载出来而不是只有当我停止下滑时候图片才被加载出来。又或者下滑时候的数据的 ajax 请求加载也是同理。这个时候我们希望即使页面在不断被滚动但是滚动 handler 也可以以一定的频率被触发譬如 250ms 触发一次这类场景就要用到另一种技巧称为节流函数throttling。节流函数只允许一个函数在 X 毫秒内执行一次。与防抖相比节流函数最主要的不同在于它保证在 X 毫秒内至少执行一次我们希望触发的事件 handler。与防抖相比节流函数多了一个 mustRun 属性代表 mustRun 毫秒内必然会触发一次 handler 同样是利用定时器看看简单的示例123456789101112131415161718192021222324252627// 简单的节流函数functionthrottle(func, wait, mustRun) {vartimeout,startTime newDate();returnfunction() {varcontext this,args arguments,curTime newDate();clearTimeout(timeout);// 如果达到了规定的触发时间间隔触发 handlerif(curTime - startTime mustRun){func.apply(context,args);startTime curTime;// 没达到触发间隔重新设定定时器}else{timeout setTimeout(func, wait);}};};// 实际想绑定在 scroll 事件上的 handlerfunctionrealFunc(){console.log(Success);}// 采用了节流函数window.addEventListener(scroll,throttle(realFunc,500,1000));上面简单的节流函数的例子可以拿到浏览器下试一下大概功能就是如果在一段时间内 scroll 触发的间隔一直短于 500ms 那么能保证事件我们希望调用的 handler 至少在 1000ms 内会触发一次。使用 rAFrequestAnimationFrame触发滚动事件