当前位置: 首页 > 科技观察

阮一峰:网页性能管理详解

时间:2023-03-19 23:37:50 科技观察

你有遇到过性能不好的网页吗?这类网页响应很慢,占用大量CPU和内存,浏览时经常卡顿,页面动画效果不流畅。你会如何反应?我的猜测是大多数用户会关闭此页面并转而访问其他站点。作为开发者,我绝对不想看到这种情况。我怎样才能提高性能?本文将详细介绍性能问题的原因以及解决方法。一、网页生成的过程要理解网页性能不好的原因,就需要了解网页是如何生成的。生成网页的过程大致可以分为五个步骤。将HTML代码转化为DOMCSS代码转化为CSSOM(CSS对象模型)结合DOM和CSSOM生成渲染树(包括每个节点的视觉信息)生成布局(layout),即合成所有渲染树的所有节点其中在屏幕上绘制布局(paint)的五个步骤,第一到第三步很快,第四和第五步比较费时间。“生成布局”(flow)和“绘制”(paint)这两个步骤统称为“render”。2.重新排列和重绘网页时,至少会渲染一次。在用户访问期间,会不断重新渲染。以下三种情况都会导致网页重新渲染。修改DOM修改样式表用户事件(如鼠标悬停、页面滚动、在输入框中键入文本、改变窗口大小等)被重新渲染,需要重新生成和重绘布局。前者称为“回流”,后者称为“重绘”。需要注意的是,“重绘”并不一定需要“重排”。比如改变某个网页元素的颜色,只会触发“重绘”,不会触发“重排”,因为布局没有改变。然而,“重排”必然导致“重绘”。例如改变网页元素的位置,会同时触发“重排”和“重绘”,因为布局发生了变化。3、对性能的影响重排重绘会不断触发,这是不可避免的。但是,它们非常耗费资源,是网页性能不佳的根本原因。提高网页性能就是降低“回流”和“重绘”的频率和成本,尽可能少地触发重新渲染。如前所述,DOM变化和样式变化都会触发重新渲染。不过,浏览器已经很聪明了,会尽量把所有的变化聚集在一起,排成一个队列,然后一次执行,尽量避免多次重新渲染。div.style.color='blue';div.style.marginTop='30px';上面代码中,div元素有两次样式变化,但是浏览器只会触发一次重排和重绘。如果写的不好,会触发两次回流和重绘。div.style.color='blue';varmargin=parseInt(div.style.marginTop);div.style.marginTop=(margin+10)+'px';上面代码设置了div元素的背景色后,第二行要求浏览器给出元素的位置,所以浏览器不得不马上回流。一般来说,在样式的写操作之后,如果有以下属性的读操作,浏览器会立即重新渲染。offsetTop/offsetLeft/offsetWidth/offsetHeightscrollTop/scrollLeft/scrollWidth/scrollHeightclientTop/clientLeft/clientWidth/clientHeightgetComputedStyle()所以,从性能的角度来说,尽量不要把读写操作放在一个语句中。//baddiv.style.left=div.offsetLeft+10+"px";div.style.top=div.offsetTop+10+"px";//goodvarleft=div.offsetLeft;vartop=div.offsetTop;div.style.left=left+10+"px";div.style.top=top+10+"px";一般规则是:样式表越简单,回流和重绘的速度就越快。回流和重绘的DOM元素级别越高,成本就越高。重新排列和重绘表格元素的成本高于div元素。4.提高性能的九个技巧有一些技巧可以降低浏览器重新渲染的频率和成本。第一个就是上一节提到的,DOM的多次读操作(或者多次写操作)应该放在一起。不要在两个读取操作之间添加写入操作。其次,如果通过重排得到某种样式,最好将结果缓存起来。避免下次使用时浏览器重新排列。第三,不要一个一个地改变样式,而是通过改变class或csstext属性一次性改变样式。//badvarleft=10;vartop=10;el.style.left=left+"px";el.style.top=top+"px";//goodel.className+="theclassname";//goodel.style.cssText+=“;左:”+左+“px;上:”+上+“px;”;第四,尝试使用离线DOM而不是真正的WebDOM来更改元素样式。比如操作DocumentFragment对象,完成后将这个对象添加到DOM中。再比如,使用cloneNode()方法对克隆节点进行操作,然后将原节点替换为克隆节点。五、先设置元素显示:none(需要1次重排重绘),然后对该节点进行100次操作,最后恢复显示(需要1次重排重绘)。这样,您有两次重新渲染,而不是可能多达100次重新渲染。第六条,对于position属性为absolute或者fixed的元素,重排的代价会比较小,因为不需要考虑它对其他元素的影响。第七,只有在必要的时候,才让元素的display属性可见,因为不可见的元素不影响重新排列和重绘。另外,带有visibility:hidden的元素只会影响重排,不会影响重绘。八、使用虚拟DOM脚本库,如React等。第九条,使用window.requestAnimationFrame()、window.requestIdleCallback()调整重新渲染(详见下文)。5、刷新率很多情况下,密集的重渲染是不可避免的,比如滚动事件的回调函数,网页动画。网络动画的每一帧都是重新渲染。低于每秒24帧的动画可以被人眼感知为暂停。一般的网页动画需要达到每秒30到60帧的频率才能比较流畅。如果能打到每秒70帧甚至80帧,那将是极其流畅的。大多数显示器的刷新率为60Hz。为了与系统保持一致,也为了省电,浏览器会按照这个频率自动刷新动画(如果可以的话)。因此,如果网页动画能达到60帧/秒,就会与显示同步刷新,达到最佳的视觉效果。这意味着对于一秒内60次重新渲染,每次重新渲染的时间不能超过16.66毫秒。一秒钟可以完成多少次重新渲染,这个指标叫做“刷新率”,英文是FPS(framepersecond)。60次重新渲染,即60FPS。#p#六、开发者工具的Timeline面板Chrome浏览器开发者工具的Timeline面板是查看“刷新率”的最佳工具。本节介绍如何使用此工具。首先,按F12打开“开发者工具”,切换到Timeline面板。左上角有一个灰色的圆点,就是录音按钮,按下它会变成红色。然后,在网页上做一些工作,再次按下按钮完成录制。Timeline面板提供两种查看方式:横条为“EventMode”,显示重新渲染各种事件所花费的时间;竖条是“FrameMode”,它显示了每一帧所花费的时间。先看“事件模式”,从中可以判断出性能问题出现在哪里,是JavaScript执行还是渲染?不同的颜色代表不同的事件。蓝色:网络通信和HTML解析黄色:JavaScript执行紫色:样式计算和布局,即重排绿色:哪些色块重绘较多,表示性能消耗在哪里。补丁越长,问题越大。Frames模式用于查看单帧耗时。每一帧的颜色列高度越低越好,也就是耗时越少。如您所见,框架模式有两个水平参考线。较低的是60FPS,在这条线以下,可以达到每秒60帧;上面是30FPS,低于这条线,可以达到每秒30张渲染。如果颜色列超过30FPS,则页面存在性能问题。另外,还可以查看某个区间的耗时情况。或者单击每个帧以查看该帧的时间构成。七、window.requestAnimationFrame()有一些JavaScript方法可以调整重新渲染,大大提高网页性能。其中最重要的是window.requestAnimationFrame()方法。它可以放置一些代码在下一次重新渲染时执行。functiondoubleHeight(element){varcurrentHeight=element.clientHeight;element.style.height=(currentHeight*2)+'px';}elements.forEach(doubleHeight);上面的代码使用循环操作将每个元素的高度增加一倍。然而,每次通过循环,读操作之后都会有一个写操作。这会在短时间内触发大量的重新渲染,这显然对网页性能不利。我们可以使用window.requestAnimationFrame()将读操作和写操作分开,把所有的写操作放在下次重新渲染。functiondoubleHeight(element){varcurrentHeight=element.clientHeight;window.requestAnimationFrame(function(){element.style.height=(currentHeight*2)+'px';});}elements.forEach(doubleHeight);页面滚动事件(scroll)监听功能,很适合使用window.requestAnimationFrame()来推迟到下一次重新渲染。$(window).on('滚动',function(){window.requestAnimationFrame(scrollHandler);});当然,最适合的场合还是网络动画。下面是一个旋转动画的示例,其中元素每帧旋转1度。varrAF=window.requestAnimationFrame;vardegrees=0;functionupdate(){div.style.transform="rotate("+degrees+"deg)";console.log('updatedtodegrees'+degrees);degrees=degrees+1;rAF(更新);}rAF(更新);八、window.requestIdleCallback()还有一个函数window.requestIdleCallback(),也可以用来调整重新渲染。它指定只有在帧结束时有空闲时间时才会执行回调函数。请求空闲回调(fn);上述代码中,函数fn只有在当前帧运行时间小于16.66ms时才会执行。否则顺延到下一帧,如果下一帧没有空闲时间,则顺延到下一帧,以此类推。它还可以接受表示指定毫秒数的第二个参数。如果在指定的时间内每个帧都没有空闲时间,则函数fn将被执行。requestIdleCallback(fn,5000);上面的代码表示函数fn最迟在5000毫秒后执行。函数fn可以接受截止日期对象作为参数。requestIdleCallback(函数someHeavyComputation(截止日期){while(deadline.timeRemaining()>0){doWorkIfNeeded();}if(thereIsMoreWorkToDo){requestIdleCallback(someHeavyComputation);}});在上面的代码中,回调函数someHeavyComputation的参数是一个deadline对象。deadline对象有一个方法和一个属性:timeRemaining()和didTimeout。(1)timeRemaining()方法timeRemaining()方法返回当前帧的剩余毫秒数。该方法只能读,不能写,会动态更新。所以你可以一直检查这个属性,如果还有时间,你可以继续执行一些任务。一旦此属性等于0,则将任务分配给下一轮requestIdleCallback。在前面的示例代码中,只要当前帧有空闲时间,就会不断调用doWorkIfNeeded方法。一旦没有空闲时间,但任务还没有完全执行完,就会分配给下一轮requestIdleCallback。(2)didTimeout属性deadline对象的didTimeout属性会返回一个布尔值,表示指定的时间是否已经过期。这意味着如果回调函数因为指定时间已过而触发,那么您将得到两个结果。timeRemaining方法返回0,didTimeout属性等于true。因此,如果回调函数被执行,有两种原因:当前帧有空闲时间,或者指定时间到了。函数myNonEssentialWork(截止日期){while((deadline.timeRemaining()>0||deadline.didTimeout)&&tasks.length>0)doWorkIfNeeded();if(tasks.length>0)requestIdleCallback(myNonEssentialWork);}requestIdleCallback(myNonEssentialWork,5000);上面的代码保证了doWorkIfNeeded函数会在以后比较空闲的时候(或者指定时间到期后)重复执行。requestIdleCallback是一个很新的函数,刚刚引入标准,目前只有Chrome支持。九、参考链接DomenicoDeFelice,HowbrowsersworkStoyanStefanov,Rendering:repaint,reflow/relayout,restyleAddyOsmani,ImprovingWebAppPerformanceWiththeChromeDevToolsTimelineandProfilesTomWiltzius,JankBustingforBetterRenderingPerformancePaulLewis,使用请求空闲回调