(点击
上方公众号
,可快速关注)
作者:
邱俊涛
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.