Web前端知识

首页 > 免费 > Web前端知识 >

前端性能——高性能滚动 scroll 及页面渲染优化(一)

来源:北京汇仁智杰科技有限公司   时间:2016-05-18   点击:

  最近在研究页面渲染及web动画的性能问题,以及拜读《CSS SECRET》(CSS揭秘)这本大作。
  本文主要想谈谈页面优化之滚动优化。
  主要内容包括了为何需要优化滚动事件,滚动与页面渲染的关系,节流与防抖,pointer-events:none 优化滚动。因为本文涉及了很多很多基础,是我自己学习记录的一个过程,如果上面列出的知识点都了然于胸了,就可以不必往下看了。
  滚动优化的由来
  滚动优化其实也不仅仅指滚动(scroll 事件),还包括了例如 resize 这类会频繁触发的事件。简单的看看:
var i = 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 页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。
  Paint:绘制,本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
  Composite:渲染层合并,由上一步可知,对页面中DOM元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
  这里又涉及了层(GraphicsLayer)的概念,GraphicsLayer 层是作为纹理(texture)上传给GPU的,现在经常能看到说GPU硬件加速,就和所谓的层的概念密切相关。但是和本文的滚动优化相关性不大,有兴趣深入了解的可以自行 google 更多。
  简单来说,网页生成的时候,至少会渲染(Layout+Paint)一次。用户访问的过程中,还会不断重新的重排(reflow)和重绘(repaint)。
  其中,用户 scroll 和 resize 行为(即是滑动页面和改变窗口大小)会导致页面不断的重新渲染。
  当你滚动页面时,浏览器可能会需要绘制这些层(有时也被称为合成层)里的一些像素。通过元素分组,当某个层的内容改变时,我们只需要更新该层的结构,并仅仅重绘和栅格化渲染层结构里变化的那一部分,而无需完全重绘。显然,如果当你滚动时,像视差网站(戳我看看)这样有东西在移动时,有可能在多层导致大面积的内容调整,这会导致大量的绘制工作。
  防抖(Debouncing)和节流(Throttling)
  scroll事件本身会触发页面的重新渲染,同时scroll事件的handler又会被高频度的触发, 因此事件的handler内部不应该有复杂操作,例如DOM操作就不应该放在事件处理中。
  针对此类高频度触发事件问题(例如页面scroll,屏幕resize,监听用户输入等),下面介绍两种常用的解决方法,防抖和节流。
  防抖(Debouncing)
  防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。
  通俗一点来说,看看下面这个简化的例子:
// 简单的防抖动函数
function debounce(func, wait, immediate) {
// 定时器变量
 var timeout;
 return function() {
// 每次触发 scroll handler 时先清除定时器
  clearTimeout(timeout);
// 指定 xx ms 后触发真正想进行的操作 handler<br/  timeout = setTimeout(func, wait);
  };
};
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
  console.log("Success");
}
// 采用了防抖动
window.addEventListener('scroll',debounce(realFunc,500));
// 没采用防抖动
window.addEventListener('scroll',realFunc);
  上面简单的防抖的例子可以拿到浏览器下试一下,大概功能就是如果 500ms 内没有连续触发两次 scroll 事件,那么才会触发我们真正想在 scroll 事件中触发的函数。
  上面的示例可以更好的封装一下:
// 防抖动函数
function debounce(func, wait, immediate) {
  return function() {
  var context = this, args = arguments;
  var later = function() {
  timeout = null;
  if (!immediate) func.apply(context, args);
  };
  var callNow = immediate !timeout;
  clearTimeout(timeout);
  timeout = setTimeout(later, wait);
  if (callNow) func.apply(context, args);
  };
};
 
var myEfficientFn = debounce(function() {
// 滚动中的真正的操作
}, 250);
// 绑定监听
window.addEventListener('resize', myEfficientFn);
  节流(Throttling)
  防抖函数确实不错,但是也存在问题,譬如图片的懒加载,我希望在下滑过程中图片不断的被加载出来,而不是只有当我停止下滑时候,图片才被加载出来。又或者下滑时候的数据的 ajax 请求加载也是同理。
  这个时候,我们希望即使页面在不断被滚动,但是滚动 handler 也可以以一定的频率被触发(譬如 250ms 触发一次),这类场景,就要用到另一种技巧,称为节流函数(throttling)。
  节流函数,只允许一个函数在 X 毫秒内执行一次,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
  与防抖相比,节流函数最主要的不同在于它保证在 X 毫秒内至少执行一次我们希望触发的事件 handler。
  与防抖相比,节流函数多了一个 mustRun 属性,代表 mustRun 毫秒内,必然会触发一次 handler ,同样是利用定时器,看看  简单的示例:
// 简单的节流函数
function throttle(func, wait, mustRun) {
 var timeout,
  startTime = new Date();
  return function() {
  var context = this,
  args = arguments,
  curTime = new Date();
  clearTimeout(timeout);
 // 如果达到了规定的触发时间间隔,触发 handler
  if(curTime - startTime >= mustRun){
 func.apply(context,args);
 startTime = curTime;
// 没达到触发间隔,重新设定定时器
}else{
timeout = setTimeout(func, wait);
}
};
};
// 实际想绑定在 scroll 事件上的 handler
function realFunc(){
console.log("Success");
}
// 采用了节流函数
window.addEventListener('scroll',throttle(realFunc,500,1000));
  上面简单的节流函数的例子可以拿到浏览器下试一下,大概功能就是如果在一段时间内scroll触发的间隔一直短于500ms,那么能保证事件我们希望调用的handler至少在1000ms内会触发一次。

网络营销推广 . 北京汇仁智杰科技有限公司!

地址:北京市昌平区回龙观龙冠大厦5层
咨询:13370157521
业务QQ:373002979
E - mail:sales @ huirenzhijie.com
企业网站备案:京ICP备15021091号-1

汇仁智杰与众不同

  • 有网络推广经验
  • 有网站建站队伍
  • 有大型网站建设经验
  • 致力于营销型网站建设
  • 始终坚持技术和服务同样重要
查看PC版网站
备案号:京ICP备15021091号-1 版权所有:汇仁智杰