阅读 882

浏览器渲染流程

围绕从输入网址到网页内容展示的过程,接着前面梳理的文章浏览器组成和多进程架构继续梳理,这里针对浏览器渲染的具体内容进行探究,主要顺序是从浏览器进程,到浏览器内核运行,再到渲染线程,再到JS引擎线程,再到JS事件循环机制。 前面文章有说到:

  • 浏览器是多进程架构,其中渲染进程负责页面渲染、JS脚的执行,页面事件处理等
  • 把从输入网址到网页内容展示的流程按照前后顺序分为两大部分:网络资源请求 和 浏览器渲染,渲染过程就是浏览器渲染进程负责的事情,即对请求到的资源进行渲染展示。 那么接下来就了解下渲染进程的渲染机制吧,这也是浏览器内核的运行机制。

浏览器渲染进程

浏览器渲染进程是多线程模型

浏览器渲染进程是多线程模型,主要包含了如下一些线程类型

  1. GUI渲染线程
    • 解析HTML,CSS,构建DOM树、构建render树,布局和绘制等
    • 界面重绘(Repaint)或回流(reflow)
  2. JS引擎线程
    • 处理JavaScript程序
    • JS引擎一直等待着任务队列中任务的到来,然后加以处理
  3. 事件触发线程 当JS引擎执行如setTimeOut代码块时,或者是其他的如网络异步请求、鼠标点击等时,会将对应任务添加到事件线程中。当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理的事件队列中,等待JS引擎空闲时处理。
  4. 定时器线程
    setInterval与setTimeout所在线程。计时完毕后,将对应事件添加到事件队列中,等待JS引擎空闲时处理。
  5. 异步网络请求线程 处理网络请求,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。等待JS引擎空闲时处理。

GUI渲染线程和JS引擎线程是互斥的,当JS引擎线程执行时,GUI渲染线程会被挂起。这是因为JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

当在解析网页时遇到script标签,即有JS代码,渲染进程会把GUI更新会被保存在一个队列中,然后让JS引擎线程来处理JS代码,等到JS程序执行完毕,JS引擎空闲进行阻塞线程态时,GUI线程才会又开始工作,GUI更新会立即被执行。所以网页中JS脚本会阻塞网页的渲染。如果JS运行时间过长,就会造成页面的渲染不连贯。

下面看下GUI渲染线程的渲染流程,即关键渲染路径。

渲染流程

关键渲染路径(Critical Rendering Path)是指浏览器将HTML,CSS,JavaScript转换为屏幕上所呈现的实际像素这期间所经历的一系列步骤。

渲染路径有五大步骤:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制

  • 浏览器获取HTML并开始构建DOM(文档对象模型 - Document Object Model)。
  • 获取CSS并构建CSSOM(CSS对象模型 - CSS Object Model)。
  • 将DOM与CSSOM结合,创建渲染树(Render Tree)。
  • 布局(layout),找到所有内容都处于网页的哪个位置。
  • 绘制(painting),浏览器开始在屏幕上绘制像素。

解析html元素,构建dom树

  • token化
    将字符串转换成Token,标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息,用于维护节点与节点之间的关系

  • 生成节点对象
    一边生成Token一边消耗Token来生成节点对象,带有结束标签标识的Token不会创建节点对象。节点对象包含了这个节点的所有属性
  • 生成DOM树
    当所有Token都生成并消耗完毕后,我们就得到了一颗完整的DOM树

每一个虚线上有一个小数字,表示构建DOM的具体步骤。可以看出:
首先生成出htmlToken,并消耗Token创建出html节点对象。
然后生成headToken并消耗Token创建出head节点对象,并将它关联到html节点对象的子节点中。
随后生成titleToken并消耗Token创建出title节点对象并将它关联到head节点对象的子节点中。
最后生成bodyToken并消耗Token创建body节点对象并将它关联到html的子节点中。
当所有Token都消耗完毕后,我们就得到了一颗完整的DOM树。

解析css,构建CSSOM树

在 HTML 中引入 CSS方式有两种方式:内联方式和外联方式。

构建CSSOM并不需要等待所有DOM都构建完毕。而是在解析HTML构建DOM时,若遇见CSS会立刻构建CSSOM。即DOM 和 CSSOM 是并行构建的。 构建CSSOM和构建DOM是可以同时进行的。不过进入下一个构建渲染树阶段必须要等待CSSOM构建完毕后才能进行。因为CSS的每个属性都可以改变CSSOM,所以会存在这样一个问题:假设前面几个字节的CSS将字体大小设置为16px,后面又将字体大小设置为14px,那么如果不把整个CSSOM构建完整,最终得到的CSSOM其实是不准确的。所以必须等CSSOM构建完毕才能进入到下一个阶段,哪怕DOM已经构建完,它也得等CSSOM,然后才能进入下一个阶段。
所以,CSSOM 会阻塞渲染,只有当 CSSOM 构建完毕后才会进入下一个阶段构建渲染树。

CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度。因为浏览器的渲染需要 render tree, render tree 需要 CSSOM 树才行,所以样式表的加载是会阻塞页面的渲染的,如果有一个外部的样式表处于下载中,那么即使 HTML 已经下载完毕,也会等待外部样式表下载并解析完毕才会开始构建 render tree

所以CSS 放头部,可以提高页面的性能。早构建早渲染。

构建 Rendering Tree

通过 DOM Tree 和 CSS Rule Tree 来构建 Rendering Tree。 Rendering Tree的每个节点都有了样式信息。

layout

根据渲染树来布局,以计算每个节点的几何信息,即在屏幕上的确切位置和大小,所有相对值都将转换为屏幕上的绝对像素。

painting

将各个节点绘制到屏幕上

JS与关键渲染路径

网页中经常会执行JS脚本。这让网页渲染的路径变得复杂了许多。
JS代码的运行机制

情况 1-普通 script(同步下载并执行)

<script src="script.js"></script>
复制代码

在解析网页时,如果遇到一个不带 defer 或 async 属性的 script 标签时,会先等CSSOM下载和构建完毕,再执行JS,等JavaScript执行完后,再继续构建DOM。

由此可见,JS不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建。 通常情况下 DOM 和 CSSOM 是并行构建的。

这是因为 JavaScript 不只是可以改 DOM,它还可以更改样式,也就是说它可以更改 CSSOM。因为不完整的 CSSOM 是无法使用的,如果 JavaScript 想访问 CSSOM 并更改它,那么在执行 JavaScript 时,必须要能拿到完整的 CSSOM。所以就导致了一个现象,如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后再继续构建 DOM。

也就是说,如果想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。不过这并不是说 script 标签必须放在底部,因为可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。

情况 2-带async属性的脚本(异步下载)

<script async src="script.js"></script>
复制代码

用于异步下载脚本文件,下载完毕立即解释执行代码。
不过这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。

与 defer 的区别在于

  • 如果已经加载好,就会开始执行
  • 在加载多个 JS 脚本的时候,async 是无顺序的加载,先下载的先执行,而 defer 是有顺序的加载,按照顺序执行。

( DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。
Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。 )

情况3-带defer属性的脚本(延迟执行)

<script defer src="script.js"></script>
复制代码

用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。 defer 与相比普通 script,有两点区别:

  • 载入 JavaScript 文件时不阻塞 HTML 的解析
  • 执行阶段被放到 HTML 标签解析完成之后

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

页面重绘和回流

重绘

当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color,这个过程叫做重绘(repaint) 重绘时机有

  • 当一个元素的外观的可见性 visibility 发生改变的时候,但是不影响布局
  • 回流会引起重绘

回流

当 render tree 中的一部分(或全部)因为元素的尺寸、布局、显示/隐藏等改变而需要重新构建,这个过程称作回流(reflow)。页面第一次加载的时候,至少发生一次回流。 回流时机有

  • 页面渲染初始化。
  • 调整窗口大小。
  • 改变字体,比如修改网页默认字体。
  • 增加或者移除样式表。
  • 内容变化,比如文本改变或者图片大小改变而引起的计算值宽度和高度改变。
  • 激活 CSS 伪类,比如 :hover
  • 操作 class 属性。
  • 脚本操作 DOM,增加删除或者修改 DOM
  • 节点,元素尺寸改变——边距、填充、边框、宽度和高度。
  • 计算 offsetWidth 和 offsetHeight 属性。
  • 设置 style 属性的值。

在回流的时候,浏览器会使 render tree 中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。因此回流必将引起重绘,而重绘不一定会引起回流。

Reflow 的成本比 Repaint 高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。

优化渲染路径

  • 常见的优化网络请求的方法有:DNS Lookup,减少重定向,避免 JS、CSS 阻塞,并行请求,代码压缩,缓存,按需加载,前端模块化
  • 要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉

页面性能分析指标

页面渲染包括从浏览器跳转,缓存检查,再到 DNS、TCP 建连,然后发起主文档请求,再到接收完最后一个字节,再到浏览器开始CSS、JS、图片的下载,最后是页面渲染和交互响应。
这个过程中有一系列节点指标,通过这些指标值可以分析出页面在各个阶段的耗时情况。

document.readyState

document.readyState 属性描述了文档的加载状态

  • uninitialized
    还未开始载入

  • "loading" document 仍在加载。

  • "interactive"
    文档已被解析,"正在加载"状态结束,DOM元素可以被访问。但是诸如图像,样式表和框架之类的子资源仍在加载。

  • "complete"
    文档和所有子资源已完成加载。表示 load 状态的事件即将被触发。

总结

  • 渲染路径有五大步骤:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制
  • GUI渲染线程与JS引擎线程互斥
  • CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度。
  • JS不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建。
  • CSS 放头部,JS 放底部可以提高页面的性能

参考资料

前端性能优化之关键路径渲染优化

你不知道的浏览器渲染原理

「性能优化」首屏时间从12.67s到1.06s,我是如何做到的?