浏览器渲染流程及其性能优化分析

渲染流程

一个页面呈现通过浏览器呈现到出来,会经过以下步骤

webkitflow.png
  1. 解析页面内容
  2. 构建DOM树
  3. 构建CSSOM树
  4. 合并DOM树和CSSOM树,生成Render-Tree(渲染树)
  5. 基于当前的viewport计算出每个元素的位置和尺寸等几何信息(Layout)
  6. 将元素信息转换成屏幕上的像素(Painting)
  7. 显示(Display)

构建DOM树

DOM树是对html文档结构的描述,它存储了html文档标签的属性和关系,每一个html标签都会存在一个对应的DOM元素,元素的嵌套层级决定了他们的上下层关系,最终所有DOM元素会构成一颗DOM树。

以下是一个简单的html页面:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

它的构造过程如下:

full-process.png
  1. 编码:浏览器处理接收到的HTML原始字节数据,根据指定的编码将字节转换成字符(Bytes —> Characters)。
  2. 提取标签:浏览器根据W3C HTML5 标准从第一步中得到的字符串中提取出各种html标签,例如,“<html>”、“<body>”,以及其他尖括号内的字符串。每个令牌都具有特殊含义和一组规则(Characters —> Tokens)。
  3. 词法分析: 根据第二部中得到的标签,根据html元素的语言、规则、属性,将其转换成一个个元素对象。
  4. DOM构建: 步骤三中创建的每一个对象都会链接在一个数据结构内,该结构会捕获原始标记中定义的父项-子项关系,如:HTML 对象是 body 对象的父项,bodyparagraph 对象的父项,依此类推。利用链接结构,构建出DOM树

CSSOM树

CSS定义了html文档可以应用的样式表,CSSOM树存储了CSS的对象模型结构。在浏览器为页面的html元素计算样式时,可以通过遍历CSSOM来查找匹配的样式表。

以以下CSS为例:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

CSSOM树产生的过程与DOM树的构建过程类似,具体如下:

cssom-construction.png

浏览器会根据CSS的语法标准,解析CSS文件,并生成相应的对象,最后构成CSSOM树,上面的CSS最终构成的CSSOM树如下:

cssom-tree.png

body是所有显示元素的根节点,它定义了font-size:16px,由于所有的元素都是body的子项,所以他们会继承body的样式(CSS的级联规则)。

注意:

以上树并非完整的 CSSOM 树,它只显示了我们决定在样式表中替换的样式。每个浏览器都提供一组默认样式(也称为“User Agent 样式”),即我们不提供任何自定义样式时所看到的样式,我们的样式只是替换这些默认样式(例如默认 IE 样式)。

构建渲染树

渲染树通过合并DOM和CSSOM得到,它只包含渲染网页所需的(需要显示的)节点,以上面的DOM树和CSSOM树为例,他们合并后如下:

render-tree-construction.png

具体步骤如下:

  1. 从 DOM 树的根节点开始遍历每个可见节点。

    • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
  2. 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。

    • 如果一个DOM节点通过 CSS进行了隐藏,那么它在渲染树中也会被忽略,例如,上例中的p标签下的span 节点就不会出现在渲染树中,因为有一个显式规则在该节点上设置了“display: none”属性(visible属性不会)

    Note: visibility: hiddendisplay: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。

  3. 生成渲染树,得到最终要显示的所有元素和它的样式。

注意:

只有同时具有 DOM 和 CSSOM 才能开始构建渲染树

布局

布局阶段会根据渲染树中得到的元素及其样式信息去计算元素在当前设备上的具体的大小、位置,所有相对测量值都转换为屏幕上的绝对像素。

以下面的代码为例:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

head中的meta标签声明了当前页面的宽度应该与设备宽度相等width=device-width,不进行缩放initial-scal=1. Body 下的第一个div采用默认布局,它的宽度为其父容器的50%,它下面的div同样采用默认布局,宽度又为它父亲节点的50%。

假如当前设备的宽度为320px,则第一个div的宽度就是160px,第二个div的宽度就是80px,最终效果如下

layout-viewport.png

绘制

在这个阶段,浏览器会根据在Layout阶段计算出的所有节点的几何信息(大小、位置),将其绘制到屏幕。

至此,页面已经可以在屏幕显示出来。

DOM & CSSOM & JS

  • 浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕

  • 同时具有 DOM 和 CSSOM 才能构建渲染树

  • 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行

  • 如果浏览器尚未完成 CSSOM 的下载和构建,此时有Javscript脚本需要运行,浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建

  • 图像不会阻止页面的首次渲染

下面是一个具体的例子:

1# <!DOCTYPE html>
2# <html>
3#   <head>
4#     <meta name="viewport" content="width=device-width,initial-scale=1">
5#     <link href="style.css" rel="stylesheet">
6#     <title>Critical Path: Script External</title>
7#   </head>
8#   <body>
9#     <p>Hello <span>web performance</span> students!</p>
10#    <div><img src="awesome-photo.jpg"></div>
11#    <script src="app.js"></script>
12#   </body>
13# </html>

在这个例子中,整个页面的加载流程如下:

  1. 浏览器解析html文档(编码—>提取标签—>词法分析)
  2. 然后开始构建DOM
  3. 第5行,发现link标签,开始加载CSS文件并创建CSSOM
  4. 第11行,发现script标签,开始加载外部脚本,阻塞DOM的构建
  5. 脚本加载完毕,开始执行脚本,此时如果
    1. CSSOM已经构建完毕,则直接执行脚本
    2. CSSOM并没有构建完毕,则等待CSSOM构建完毕,然后执行脚本
  6. 脚本执行完毕,继续DOM构建
  7. 利用构建好的DOM+CSSOM,穿件渲染树
  8. 绘制并显示

渲染过程性能优化

优化关键渲染路径

关键渲染路径指的是 HTML 标记、CSS 和 JavaScript,图像不会阻止页面的首次渲染。通过优化关键渲染路径,可以缩短首次渲染页面的时间。

可以通过以下维度进行优化:

  1. 减少资源数量、字节大小。

    如通过压缩css和js文件来减小他们的字节数,通过合并文件来减少http请求的数量,以此来提高资源加载的速度

  2. 尽早加载CSS资源

    由于CSSOM会阻塞渲染,只有DOM和CSSOM加载完毕后渲染才开始执行,因此应该尽量早的加载CSS资源

  3. 利用media属性声明CSS的使用场景

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

    上面的例子中,声明了media的值为orientation:portrait,则这个style.css文件只有在横屏状况下才会使用,其他状况都不会阻塞渲染。

    关于media的更多资料,可以查看这里

  4. 将脚本标记为异步

    默认情况下,所有 JavaScript 都会阻止解析器,向 script 标记添加异步关键字可以指示浏览器在等待脚本加载期间不阻止 DOM 构建,如下

    <script src="app.js" async></script>
    

提升交互性能

交互或者动画都会出发页面的重绘,他会影响用户的操作流畅性体验。当前主流的设备的帧率都是60fps,因此网页应用渲染完一帧的速率就应该低于16ms这样才不会掉帧。在这16ms中,浏览器需要完成以下工作:

frame-full.jpg

执行JavaScript脚本(增删改dom或style) -> 计算节点样式 -> 计算节点的集合信息(位置、大小) -> 像素填充 -> 图层合并​

Javascript和CSS是由开发者提供的,其他步骤都完全由浏览器控制,因此,我们的脚本的处理时间应该低于16ms才能保证页面的重绘在16ms内完成,一般来说,留给JavaScript执行的时间大概在10ms左右。

使用 requestAnimationFrame 来实现视觉变化

JavaScript执行的最佳时机是在屏幕开始绘制新一帧的开头。 requestAnimationFrame接受一个函数,并保证该函数在新的一帧的开头被调用,我们可以使用这个方法来处理动画。下面是一个小的示例:

var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

function move(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(move);
  }
}

window.requestAnimationFrame(move);

不推荐使用 setTimeoutsetInterval 来执行动画之类的视觉变化的原因在于,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。

settimeout.png

减少/避免布局操作

当元素的“layout”属性发生了改变,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排”页面。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

常见的几何属性有:

bottom, direction,display,float,font-size,font-weight,height,left,margin,padding,position,right,top,width...

可以通过https://csstriggers.com/查看更多信息。

避免强制同步布局

正常的页面更新流程是JavaScript 运行 -> 计算样式 -> 布局, 但是在以下场景,JavaScript会强制浏览器提前进行布局:

var box = document.getElementById("example-dom");
box.classList.add('fiori');         //修改样式
console.log(box.offsetHeight);      //读取高度

在给box节点添加新的样式ful-screen之前,自上一帧的所有旧布局值是已知的,浏览器为了优化性能,会将更新操作暂缓到队列批量处理;但是,程序立马请求查看节点的高度box.offsetHeight,为了获取准确的数据,浏览器必须立刻应用样式更改,然后运行布局,这是不必要的,如果有大量的这种操作(处理动画时,往往会不停的修改样式,应该避免写与读操作相间触发),将会带来巨大的开销。

正确的做法是应该先获取高度,然后再进行样式的更改

var box = document.getElementById("example-dom");
console.log(box.offsetHeight);      //读取高度
box.classList.add('fiori');         //修改样式

反例:

for(var i=0;i<p.length;i++){
  p[i].style.width = d.offsetWidth + 'px'; //获取d的宽度并赋值给p[i]
}

正例:

var w = d.offsetWidth; // 提前获取好d的宽度
for(var i=0;i<p.length;i++){
  p[i].style.width = w + 'px'; //通过变量w给p[i]赋值,循环中只有写操作,不会强制同步布局
}

后台运行复杂/耗时逻辑

JavaScript 在浏览器的主线程上运行,恰好与样式计算、布局以及许多情况下的绘制一起运行。如果 JavaScript 运行时间过长,就会阻塞这些其他工作,可能导致帧丢失。对于一些计算密集的操作,建议使用web worker进行处理,不要占用浏览器的主进程

减少绘制区域

绘制并非总是绘制到内存中的单个图像。事实上,在必要时浏览器可以绘制到多个图像或合成器层,类似于Photoshop图层的概念,我们可以创建不同的图层绘制图像,最后将他们合并。

利用这个特性,可以将经常需要重绘的部分单独放在一个图层,避免整个页面收到影响。

使用 will-change CSS 属性可以为元素创建一个新的图层, will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。

.moving-element {
  will-change: transform;
}

注意:

不要创建太多图层,因为每层都需要内存和管理开销。

其他

  • 不要使用过于复杂的css选择器
  • 精简DOM节点数量

工具

性能监控工具

  • Chrome DevTool(强力推荐,以后有机会会单独介绍)
  • Lighthouse

参考文献

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • 大家都知道万维网的应用层使用了HTTP协议,并且用浏览器作为入口访问网络上的资源。用户在使用浏览器访问一个网站时需...
    SylvanasSun阅读 2,149评论 1 12
  • 发送 & 接收信息 数据是以“数据包”的形式通过互联网发送,而数据包以字节为单位。当你编写一些 HTML、CSS ...
    mongofeng阅读 917评论 0 0
  • 发送 & 接收信息 数据是以“数据包”的形式通过互联网发送,而数据包以字节为单位。当你编写一些 HTML、CSS ...
    IT界中小PQ阅读 406评论 0 0
  • 用户在使用浏览器访问一个网站时需要先通过HTTP协议向服务器发送请求,之后服务器返回HTML文件与响应信息。这时,...
    dosher_多舍阅读 852评论 0 1
  • 背景(做前端永远也跨不过去了就是性能优化) 公司目前有大量前后端耦合项目,浏览器的首屏加载速度非常的慢,在不段优化...
    天一呀阅读 1,156评论 0 4