专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
前端早读课  ·  【早阅】Figma MCP ... ·  21 小时前  
龙视新闻联播  ·  龙江最美飘雪时|早春升温日 赏冰乐雪时 ·  昨天  
宝山消防支队  ·  以案为例 |《油锅起火怎么办?》 ·  昨天  
前端早读课  ·  【第3459期】两款 AI 编程助手 ... ·  昨天  
前端之巅  ·  Dagger:我们用 GO 和 ... ·  2 天前  
51好读  ›  专栏  ›  前端大全

如何提升页面渲染效率

前端大全  · 公众号  · 前端  · 2017-10-05 20:00

正文

(点击 上方公众号 ,可快速关注)


作者: 邱俊涛

icodeit.org/2017/02/frontend-page-performance-tuning/

如有好文章投稿,请点击 → 这里了解详情


Web页面的性能


我们每天都会浏览很多的Web页面,使用很多基于Web的应用。这些站点看起来既不一样,用途也都各有不同,有在线视频,Social Media,新闻,邮件客户端,在线存储,甚至图形编辑,地理信息系统等等。虽然有着各种各样的不同,但是相同的是,他们背后的工作原理都是一样的:


  • 用户输入网址

  • 浏览器加载HTML/CSS/JS,图片资源等

  • 浏览器将结果绘制成图形

  • 用户通过鼠标,键盘等与页面交互



这些种类繁多的页面,在用户体验方面也有很大差异:有的响应很快,用户很容易就可以完成自己想要做的事情;有的则慢慢吞吞,让焦躁的用户在受挫之后拂袖而去。毫无疑问,性能是影响用户体验的一个非常重要的因素,而影响性能的因素非常非常多,从用户输入网址,到用户最终看到结果,需要有很多的参与方共同努力。这些参与方中任何一个环节的性能都会影响到用户体验。


  • 宽带网速

  • DNS服务器的响应速度

  • 服务器的处理能力

  • 数据库性能

  • 路由转发

  • 浏览器处理能力


早在2006年,雅虎就发布了提升站点性能的指南,Google也发布了类似的指南。而且有很多工具可以和浏览器一起工作,对你的Web页面的加载速度进行评估:分析页面中资源的数量,传输是否采用了压缩,JS、CSS是否进行了精简,有没有合理的使用缓存等等。


如果你需要将这个过程与CI集成在一起,来对应用的性能进行监控,我去年写过一篇相关的博客。


本文打算从另一个角度来尝试加速页面的渲染:浏览器是如何工作的,要将一个页面渲染成用户可以看到的图形,浏览器都需要做什么,哪些过程比较耗时,以及如何避免这些过程(或者至少以更高效的方式)。


页面是如何被渲染的


说到性能优化,规则一就是:


If you can’t measure it, you can’t improve it. – Peter Drucker


根据浏览器的工作原理,我们可以分别对各个阶段进行度量。


图片来源:http://dietjs.com/tutorials/host#backend


像素渲染流水线


  • 下载HTML文档

  • 解析HTML文档,生成DOM

  • 下载文档中引用的CSS、JS

  • 解析CSS样式表,生成CSSOM

  • 将JS代码交给JS引擎执行

  • 合并DOM和CSSOM,生成Render Tree

  • 根据Render Tree进行布局layout(为每个元素计算尺寸和位置信息)

  • 绘制(Paint)每个层中的元素(绘制每个瓦片,瓦片这个词与GIS中的瓦片含义相同)

  • 执行图层合并(Composite Layers)


使用Chrome的DevTools – Timing,可以很容易的获取一个页面的渲染情况,比如在Event Log页签上,我们可以看到每个阶段的耗时细节(清晰起见,我没有显示Loading和Scripting的耗时):



上图中的Activity中,Recalculate Style就是上面的构建CSSOM的过程,其余Activity都分别于上述的过程匹配。


应该注意的是,浏览器可能会将Render Tree分成好几个层来分别绘制,最后再合并起来形成最终的结果,这个过程一般发生在GPU中。


Devtools中有一个选项:Rendering - Layers Borders,打开这个选项之后,你可以看到每个层,每个瓦片的边界。浏览器可能会启动多个线程来绘制不同的层/瓦片。



Chrome还提供一个Paint Profiler的高级功能,在Event Log中选择一个Paint,然后点击右侧的Paint Profiler就可以看到其中绘制的全过程:



你可以拖动滑块来看到随着时间的前进,页面上元素被逐步绘制出来了。我录制了一个我的知乎活动页面的视频,不过需要翻墙。


视频地址:https://youtu.be/gley7VZFx_I


常规策略


为了尽快的让用户看到页面内容,我们需要快速的完成DOM+CSSOM - Layout - Paint - Composite Layers的整个过程。一切会阻塞DOM生成,阻塞CSSOM生成的动作都应该尽可能消除,或者延迟。


在这个前提下,常见的做法有两种:


分割CSS


对于不同的浏览终端,同一终端的不同模式,我们可能会提供不同的规则集:


@ media print {

html {

font - family : 'Open Sans' ;

font - size : 12px ;

}

}

@ media orientation : landscape {

//

}


如果将这些内容写到统一个文件中,浏览器需要下载并解析这些内容(虽然不会实际应用这些规则)。更好的做法是,将这些内容通过对link元素的media属性来指定:


< link href = "print.css" rel = "stylesheet" media = "print" >

< link href = "landscape.css" rel = "stylesheet" media = "orientation:landscape" >


这样,print.css和landscape.css的内容不会阻塞Render Tree的建立,用户可以更快的看到页面,从而获得更好的体验。


高效的CSS规则


CSS规则的优先级


很多使用SASS/LESS的开发人员,太过分的喜爱嵌套规则的特性,这可能会导致复杂的、无必要深层次的规则,比如:


# container {

p {

. title {

span {

color : # f3f3f3 ;

}

}

}

}


在生成的CSS中,可以看到:


# container p . title span {

color : # f3f3f3 ;

}


而这个层次可能并非必要。CSS规则越复杂,在构建Render Tree时,浏览器花费的时间越长。CSS规则有自己的优先级,不同的写法对效率也会有影响,特别是当规则很多的时候。这里有一篇关于CSS规则优先级的文章可供参考。


使用GPU加速


很多动画都会定时执行,每次执行都可能会导致浏览器的重新布局,比如:


@ keyframes my {

20 % {

top : 10px ;

}

50 % {

top : 120px ;

}

80 % {

top : 10px ;

}

}


这些内容可以放到GPU加速执行(GPU是专门设计来进行图形处理的,在图形处理上,比CPU要高效很多)。可以通过使用transform来启动这一特性:


@ keyframes my {

20 % {

transform : translateY ( 10px );

}

50 % {

transform : translateY ( 120px );

}

80 % {

transform : translateY ( 10px );

}

}


异步JavaScript


我们知道,JavaScript的执行会阻塞DOM的构建过程,这是因为JavaScript中可能会有DOM操作:


var element = document . createElement ( 'div' );

element . style . width = '200px' ;

element . style . color = 'blue' ;

body . appendChild ( element );


因此浏览器会等等待JS引擎的执行,执行结束之后,再恢复DOM的构建。但是并不是所有的JavaScript都会设计DOM操作,比如审计信息,WebWorker等,对于这些脚本,我们可以显式地指定该脚本是不阻塞DOM渲染的。


通过Timeline,有时候你会看到这样的警告:



比如访问一个元素的offsetWidth(布局宽度)属性时,浏览器需要重新计算(重新布局),然后才能返回最新的值。如果这个动作发生在一个很大的循环中,那么浏览器就不得不进行多次的重新布局,这可能会产生严重的性能问题:


for(var i = 0; i list.length; i++) {

  list[i].style.width = parent.offsetWidth + 'px';

}


正确的做法是,先将这个值读出来,然后缓存在一个变量上(触发一次重新布局),以便后续使用:


var parentWidth = parent.offsetWidth;

for(var i = 0; i list.







请到「今天看啥」查看全文