目前正在研究页面渲染和网页动画的性能问题,阅读《CSS SECRET》(CSS揭秘)这部大作。这篇文章主要想说说页面优化的滚动优化。主要内容包括为什么要优化滚动事件,滚动和页面渲染的关系,节流和防抖,pointer-events:none优化滚动。由于本文涉及的基础知识较多,大家可以根据以上知识点有选择地跳转到相应的地方阅读。滚动优化的由来其实,滚动优化不仅仅指滚动(滚动事件),还包括resize等频繁触发的事件。简单看一下:vari=0;window.addEventListener('scroll',function(){console.log(i++);},false);输出如下:绑定scroll、resize等事件时,发生时,触发非常频繁,间隔很近。如果事件涉及到大量的位置计算、DOM操作、元素重绘等,而这些工作不能在下一次滚动事件触发前完成,浏览器就会掉帧。另外,用户的鼠标滚动往往是连续的,会不断触发scroll事件,导致掉帧扩大,增加浏览器CPU占用,影响用户体验。在滚动事件中绑定回调也有很多应用场景。广泛应用于图片懒加载、下滑自动加载数据、侧边浮动导航栏等。当用户浏览网页时,流畅的滚动是用户体验中经常被忽视但至关重要的部分。当滚动正常时,用户会觉得应用程序非常流畅和愉悦。相反,沉重、不自然的滚动会给用户带来极大的不适。滚动和页面渲染的关系为什么要优化滚动事件?因为它影响性能。那么它会影响什么样的性能呢?嗯...这要从决定页面性能的因素说起。我觉得我们搞技术一定要追本溯源。不要看别人的文章说滚动事件会卡顿,说一堆解法优化技巧被视为宝藏。我们需要的不是主义,而是批评,去源头多看。从问题出发,一步步找到底,很容易找到问题的症结所在。只有这样,解法才容易被记住。教了很多废话,不喜欢就无视吧。回到正题,要想找到优化的入口,就必须知道问题出在哪里。对于页面优化,那么我们需要知道页面的渲染原理:浏览器渲染原理我在上一篇文章中也会详细讲到,但是更多的是从动画渲染的角度:《【Web动画】CSS3 3D 行星运转 && 浏览器渲染原理》。想了想,再简单描述一下。我发现每次复习这些知识点,都有新的收获。这次换一张图片,以chrome为例,展示一个网页。以下步骤如下:JavaScript:一般来说,我们会使用JavaScript来实现一些视觉上的变化。比如制作动画或者给页面添加一些DOM元素。style:计算样式,这个过程就是根据CSS选择器为每个DOM元素匹配对应的CSS样式。在这一步之后,确定应该将哪些CSS样式规则应用于每个DOM元素。Layout:布局,上一步确定了每个DOM元素的样式规则,这一步是具体计算每个DOM元素最终显示在屏幕上的大小和位置。网页中元素的布局是相对的,一个元素布局的变化会引发其他元素布局的变化。例如,一个元素的宽度变化会影响它的子元素的宽度,而它的子元素宽度的变化会继续影响它的孙元素。所以对于浏览器来说,布局过程经常发生。绘画:绘画本质上是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,即一个DOM元素的所有视觉效果。通常,此绘制过程是在多个图层上完成的。Composite:渲染层合并。从上一步可以看出,页面中DOM元素的绘制是在多层上进行的。在完成每一层的绘制过程后,浏览器将所有层按逻辑顺序组合成一层显示在屏幕上。这个过程对于有重叠元素的页面尤为重要,因为一旦图层合并顺序错误,元素就会显示异常。这也涉及到图层(GraphicsLayer)的概念。GraphicsLayer图层作为纹理上传到GPU。现在经常看到GPU硬件加速跟所谓layer的概念密切相关。不过和本文的滚动优化关系不大。有兴趣深入了解的可以自行google更多。简单的说,当一个网页生成时,它至少会渲染(Layout+Paint)一次。在用户访问期间,会不断回流和重绘。其中,用户的滚动和调整大小行为(即滑动页面和改变窗口大小)会导致页面不断重新渲染。当您滚动页面时,浏览器可能需要在这些层(有时称为合成层)中绘制一些像素。通过元素分组,当某一层的内容发生变化时,我们只需要更新该层的结构,只对渲染层结构变化的部分进行重绘和栅格化,而不需要完全重绘。显然,如果滚动时有东西移动,比如视差站点(戳我看看),就有可能造成大面积的多层内容调整,会造成大量的绘画工作。防抖(Debouncing)和节流(Throttling)scroll事件本身会触发页面的重新渲染,scroll事件的handler会被频繁触发,所以事件handler内部应该没有复杂的操作,比如作为DOM操作。不应放置在事件处理程序中。针对此类高频触发事件问题(如页面滚动、屏幕缩放、监听用户输入等),下面介绍两种常用的解决方案,防抖和节流。去抖动(Debouncing)去抖动技术可以将多个顺序调用合并为一个,即指定某个事件在一定时间内被触发的次数。通俗地说,看看下面这个简化的例子://简单的去抖函数functiondebounce(func,wait,immediate){//定时器变量returnfunction(){//每次触发scrollhandler时清空TimerclearTimeout(timeout);//指定xxms触发你真正想做的操作handlertimeout=setTimeout(func,wait);};};//实际想要的在滚动事件上绑定处理函数realFunc(){console.log("Success");}//采用防抖window.addEventListener('scroll',debounce(realFunc,500));//不要使用防抖window.addEventListener('scroll',realFunc);以上是简单的防抖例子,可以在浏览器中测试。作用是如果scroll事件在500ms内连续两次没有被触发,那么scroll事件中我们真正要触发的函数就会被触发。上面的例子可以更好的封装一下://debouncefunctionfunctiondebounce(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);节流防抖功能确实不错,但是也有问题,比如图片懒加载。我希望在滑动的过程中图片能一直加载,而不是只有我停止滑动的时候才加载图片。或者滑动时ajax请求加载数据也是一样的。这时候我们希望即使页面一直在滚动,也能以一定的频率(比如每250ms一次)触发scrollhandler。在这种情况下,会使用另一种技术,称为节流功能(throttling)。节流函数,只允许一个函数在X毫秒内执行一次。与debounce相比,throttling函数的主要区别在于它保证在X毫秒内至少执行一次我们想要触发的事件处理程序。与防抖相比,节流功能多了一个mustRun属性,意思是在mustRun的毫秒内,会触发一次handler,同时也使用了timer。看一个简单的例子://简单的节流函数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;//如果没有达到触发时间间隔,则重置timer}else{timeout=setTimeout(func,wait);}};};//其实是想绑定处理函数realFunc(){console.log("Success");}//节流函数window.addEventListener('滚动',油门(realFunc,500,1000));上面一个简单节流功能的例子,可以在浏览器中进行测试。大体的作用是如果一段时间内scroll的触发间隔小于500ms,那么可以保证我们要调用的事件处理器在1000ms内至少触发一次。使用rAF(requestAnimationFrame)触发滚动事件。上面介绍的抖动和节流方式都是使用定时器setTimeout,但是如果页面只需要兼容高版本的浏览器或者移动端的应用,或者页面需要追求高精度的效果,可以使用浏览器的原生方法rAF(requestAnimationFrame)。requestAnimationFramewindow.requestAnimationFrame()该方法用于在页面重绘前通知浏览器调用指定函数。这个方法接受一个函数作为参数,它会在重绘之前被调用。rAF常用于制作网页动画,准确控制页面的帧刷新渲染,使动画效果更流畅。当然,它的作用不仅限于动画制作。我们可以利用它的特性,把它当作一个定时器。(当然不是定时器)一般来说,rAF每秒调用60次,即1000/60,触发频率约为16.7ms。(在进行复杂操作时,当发现无法保持60fps的频率时,会降低频率到30fps,以保持帧数稳定。)简单来说,使用requestAnimationFrame触发滚动事件就相当于上面这样:throttle(func,xx,1000/60)//xx表示事件处理器在xxms内不会重复触发简单例子如下:varticking=false;//rAF触发锁functiononScroll(){if(!ticking){requestAnimationFrame(realFunc);ticking=true;}}functionrealFunc(){//dosomething...console.log("Success");ticking=false;}//滚动事件监听window.addEventListener('scroll',onScroll,错误的);上面简单的使用rAF的例子可以在浏览器中进行测试。作用是在滚动过程中,以16.7ms的频率一直触发事件处理器。使用requestAnimationFrame既有优点也有缺点。首先,我们要考虑它的兼容性。其次,因为它只能以16.7ms的频率触发,也就是说它的可调性很差。但是当用于更复杂的场景时,rAF可能比throttle(func,xx,16.7)工作得更好,性能更好。总结Anti-shake:防抖技术可以将多个顺序调用合并为一个,即在一定时间内,指定事件被触发的次数。节流功能:X毫秒内只允许执行一个功能。只有在上一次函数执行后经过了您指定的时间间隔后,才能进行下一次函数调用。rAF:每16.7ms触发一个处理程序,这会降低可控性,但会提高性能和准确性。简化scroll中的操作上面介绍的方法就是如何优化scroll事件的触发,避免scroll事件过度消耗资源。但本质上,我们应该尽量简化scroll事件的handler,初始化一些不依赖于scroll位置变化的变量和计算等,应该在scroll事件之外提前准备好。建议如下:避免在滚动事件中修改样式属性/从滚动事件中剥离样式操作。输入事件处理程序,如滚动/触摸事件处理,将在requestAnimationFrame之前被调用并执行。因此,如果在滚动事件处理程序中修改样式属性,这些操作将被浏览器暂时存储起来。那么在调用requestAnimationFrame的时候,如果一开始就读取了style属性,那么这就会导致浏览器触发强制同步布局。尝试使用pointer-events:none在滑动期间禁用鼠标事件。大多数人可能不知道这个属性,嗯,那它是干什么用的呢?pointer-events是一个CSS属性,可以有多个不同的值,property的一部分值只与SVG有关。这里只关注pointer-events:none的情况,大致意思是禁止鼠标行为。应用该属性后,鼠标点击、悬停等功能将失效,即该元素不会成为鼠标事件的目标。可以按F12打开开发者工具面板,在标签中添加pointer-events:none样式,然后在页面上体验一下效果,发现所有的鼠标事件都被禁止了。那它是做什么用的呢?pointer-events:none可用于增加滚动时的帧速率。事实上,当滚动时,鼠标悬停在某些元素上,触发它们的悬停效果,但是这些效果通常不会被用户注意到,并且很可能导致滚动问题。将pointer-events:none应用于body元素会禁用包括悬停在内的鼠标事件,从而提高滚动性能。...滚动结束时,删除此属性。您可以查看此演示(https://dl.dropboxusercontent.com/u/2272348/codez/expensivescroll/demo.html)页面。上面关于pointer-events:none可以用来提高滚动时帧率的说法是摘自pointer-events-MDN,还有一篇专门介绍这项技术的文章:使用pointer-events:none实现60fps滚动。这就是结局?这不,张新旭有专门的文章探讨pointer-events:none是否真的可以加快滚动性能,并提出了自己的问题:pointer-events:none是否提高了页面滚动时的绘制性能?结论不同的人有不同的看法。当使用pointer-events:none时,要根据业务本身来决定。拒绝主义,多追根溯源,实践后再做决定。其他参考(都是好文章,值得一读):实例分析DebouncingandThrottlingWirelessPerformanceOptimization:CompositeJavascriptHigh-PerformanceAnimationandPageRenderingGoogleDevelopers–RenderingPerformanceWebHigh-PerformanceAnimation[编辑推荐]ASP.NET页面优化:8x渲染引擎性能提升分析及前端优化页面渲染机制
