要点
- Chrome 浏览器是多进程的,每个标签页一个进程,每个进程内有多个线程,其中最重要的就是渲染线程
- 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOM,DOM 和 CSSOM 合并就产生了 Render Tree。有了 RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上
- 浏览器使用流式布局模型 (Flow Based Layout),对 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一
- 回流 (reflow):当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流
- 重绘 (repaint):当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘
- 回流必将引起重绘,重绘不一定会引起回流。回流比重绘的代价要更高
- “阻塞渲染” 是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪
- 关键渲染路径的个人理解
-
进入页面后,浏览器开始解析 (构建对象模型),HTML 解析为 DOM,CSS 解析为 CSSOM。这两个会并行解析
-
DOM 和 CSSOM 会组装为 Render Tree 进行页面渲染,渲染树将移除不需要显示的标签 (如 head),或被 CSS 标记为不显示 (display: none) 的标签,继而进行布局,输出一个个盒模型,然后进行绘制
-
可以通过媒体查询等方式将一些 CSS 资源设置为不阻塞渲染。但无论是否阻塞渲染,浏览器都会下载全部的 CSS 资源
-
在浏览器解析过程中,如果遇到了 script 标签:
- script 是行内脚本,或是没有 defer 属性的外联脚本,或是没有 async 属性的外联脚本
- 阻塞 DOM 解析:脚本后面的 HTML 将停止解析
- 等待 CSSOM 解析完毕:会等待所有的 CSS 下载完毕,并且 CSSOM 构建完毕
- script 是有 defer 属性的外联脚本
- 阻塞 DOM 解析,并且等到 HTML 解析完毕后 (DOMContentLoaded 事件之后) 执行
- defer-scripts 仍然会按照 HTML 中的顺序依次执行
- script 是有 async 属性的外联脚本
- 与 HTML 并行解析,解析完毕后立即执行,执行时将阻塞 HTML 解析
- async-scripts 不会按照 HTML 中的顺序依次执行
- 有 defer 和 async 属性的外联脚本如果到了该执行的时候,CSSOM 还没有构建完毕,仍然会等待 CSSOM 构建完毕
- script 是行内脚本,或是没有 defer 属性的外联脚本,或是没有 async 属性的外联脚本
-
浏览器
浏览器组件
- 用户界面:包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面
- 浏览器引擎:在用户界面和呈现引擎之间传送指令
- 渲染引擎 (呈现引擎):负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程
- 网络:用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现
- JavaScript 解释器:用于解析和执行 JavaScript 代码。例如 Chrome 的 V8 引擎
- 用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法
- 数据存储:这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库” (Web SQL),这是一个完整(但是轻便)的浏览器内数据库
浏览器的进程和线程
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 单线程和多线程,都是指在一个进程内的单和多
- Chrome 浏览器是多进程的,每一个 tab 页就是一个独立的浏览器进程(并非绝对,比如 Chrome 会把空标签页的进程进行合并)。每个进程内可以有多个线程,通常由以下常驻线程组成:
- GUI 渲染线程
- GUI 渲染线程负责渲染浏览器界面 HTML 元素,当界面需要重绘 (Repaint) 或由于某种操作引发回流 (reflow) 时,该线程就会执行。在 Javascript 引擎运行脚本期间,GUI 渲染线程都是处于挂起状态的,也就是说被冻结了
- JavaScript 引擎线程
- JS 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果 JS 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突;如果 JS 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果,当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JS 在最初就选择了单线程执行
- GUI 渲染线程与 JS 引擎线程互斥的,是由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致。当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。由于 GUI 渲染线程与 JS 执行线程是互斥的关系,当浏览器在执行 JS 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉
- 定时触发器线程
- 浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此通过单独线程来计时并触发定时是更为合理的方案
- 事件触发线程
- 当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理
- 异步 http 请求线程
- 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JS 引擎的处理队列中等待处理
- GUI 渲染线程
渲染 (呈现)
浏览器的渲染引擎
- Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的呈现引擎
- Safari 和 Chrome 浏览器使用的都是 WebKit。WebKit 是一种开放源代码呈现引擎,起初用于 Linux 平台,随后由 Apple 公司进行修改,从而支持苹果机和 Windows
渲染流程 (以 webkit 为例)
用户请求的 HTML 文本 (text/html) 通过浏览器的网络层到达渲染引擎后,渲染工作开始。内容的大小一般限制在 8000 个块以内(整理笔记时发现,也有“每次通常渲染不会超过 8K 的数据块”一说)。
渲染流程有四个主要步骤:
- 解析 HTML 生成 DOM 树:渲染引擎首先解析 HTML 文档,生成 DOM 树
- 构建 Render 树:接下来不管是内联式,外联式还是嵌入式引入的 CSS 样式会被解析生成 CSSOM 树,根据 DOM 树与 CSSOM 树生成另外一棵用于渲染的树 - 渲染树 (Render tree)
- 布局 Render 树:然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
- 绘制 Render 树:最后遍历渲染树并用 UI 后端层将每一个节点绘制出来
以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有 HTML 都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。
基础流程图例:
webkit 引擎的详细流程图例:
渲染细节
-
生成 DOM 树
- DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。DOM 树的根节点就是 document 对象
- DOM 树的生成过程中可能会被 CSS 和 JS 的加载执行阻塞。当 HTML 文档解析过程完毕后,浏览器继续进行标记为 deferred 模式的脚本加载,然后就是整个解析过程的实际结束触发 DOMContentLoaded 事件,并在 async 文档文档执行完之后触发 load 事件
-
生成 Render 树
- 生成 DOM 树的同时会生成样式结构体 CSSOM(CSS Object Model)Tree,再根据 CSSOM 和 DOM 树构造渲染树 Render Tree,渲染树包含带有颜色,尺寸等显示属性的矩形,这些矩形的顺序与显示顺序基本一致。从 MVC 的角度来说,可以将 Render 树看成是 V,DOM 树与 CSSOM 树看成是 M,C 则是具体的调度者,比 HTMLDocumentParser 等
- 可以这么说,没有 DOM 树就没有 Render 树,但是它们之间不是简单的一对一的关系。Render 树是用于显示,那不可见的元素当然不会在这棵树中出现了,譬如 <head>。除此之外,display 等于 none 的也不会被显示在这棵树里头,但是 visibility 等于 hidden 的元素是会显示在这棵树里头的
-
DOM 树和 Render 树的关系
-
DOM 对象类型很丰富,什么 head、title、div,而 Render 树相对来说就比较单一了,毕竟它的职责就是为了以后的显示渲染用嘛。Render 树的每一个节点我们叫它渲染器 renderer
-
**有一些 DOM 元素对应多个可视化对象。**它们往往是具有复杂结构的元素,无法用单一的矩形来描述。例如,“select”元素有 3 个渲染器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的渲染器而添加。另一个关于多渲染器的例子是格式无效的 HTML。根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。如果出现了混合内容,则应创建匿名的 block 渲染器,以包裹 inline 元素
-
有一些渲染对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。浮动定位和绝对定位的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架
-
-
布局与绘制
- 上面确定了 renderer 的样式规则后,然后就是重要的显示元素布局了。当 renderer 构造出来并添加到 Render 树上之后,它并没有位置跟大小信息,为它确定这些信息的过程,接下来是布局 (layout)
- 浏览器进行页面布局基本过程是以浏览器可见区域为画布,左上角为 (0,0) 基础坐标,从左到右,从上到下从 DOM 的根节点开始画,首先确定显示元素的大小跟位置,此过程是通过浏览器计算出来的,用户 CSS 中定义的量未必就是浏览器实际采用的量。如果显示元素有子元素得先去确定子元素的显示信息
- 布局阶段输出的结果称为 box 盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位
- 在绘制 (painting) 阶段,渲染引擎会遍历 Render 树,并调用 renderer 的 paint () 方法,将 renderer 的内容显示在屏幕上。绘制工作是使用 UI 后端组件完成的。
-
回流与重绘
- (见下一节)
回流与重绘
概念
- 回流 (reflow):当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的
DOM
元素 - 激活
CSS
伪类(例如::hover
) - 查询某些属性或调用某些方法
- 重绘 (repaint):当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘
- 回流必将引起重绘,重绘不一定会引起回流。回流比重绘的代价要更高
避免回流与重绘
CSS
- 避免使用`table`布局
- 尽可能在`DOM`树的最末端改变`class`
- 避免设置多层内联样式
- 将动画效果应用到`position`属性为`absolute`或`fixed`的元素上
- 避免使用`CSS`表达式(例如:`calc()`)
- JS
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性 - 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中。也可以先为元素设置display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行的DOM
操作不会引发回流和重绘 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
- 避免频繁操作样式,最好一次性重写
关键渲染路径与阻塞渲染
部分网络资料
- HTML 和 CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有 DOM,我们就没有可渲染的内容。
- CSS 会阻塞 DOM 的渲染,但解析会正常进行;JS 会阻塞 DOM 的解析和渲染
- 存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。也就是说,遇到了 CSS 资源时,浏览器会停止 DOM 的渲染直到 CSSOM 构建完毕;JS 的执行会等待 CSS 加载完毕并且 CSSOM 构建完毕后才会继续执行。比如
- 这是由于 Render Tree 由 DOM 和 CSSOM 共同组合得到,CSS 不加载完毕就继续渲染会导致渲染前后的页面效果不一致,导致用户体验比较差
- JS 可以操作 DOM 和 CSSOM,所以为了保证 JS 的正确执行,JS 将等待 CSSOM 构建完毕
- 因此,CSS 在 HTML 中的位置应当尽可能靠上,JS 位置尽可能靠下
- 浏览器在加载 JS 时,如果 script 标签没有 defer 或者 async,浏览器会立即加载并执行脚本(如果 CSSOM 没有构建完毕则会等待 CSSOM 构建),不等待后续的 HTML 渲染完毕
defer
和async
可以使 script 异步加载,也就是说并不会阻塞解析,但执行时机不一致。注意:defer 和 async 这两个属性对行内 script 标签是无效的- defer:在 document 解析完毕并且 defer-scripts 加载完毕后,执行 defer-scripts,再触发 DOMContentLoaded 事件 (HTML 解析完成事件)
- async:脚本加载好后立即执行,可能在 DOMContentLoaded 事件之前或之后执行,但一定在 load 事件之前执行
来自 Google 的相关资料
优化关键渲染路径是指优先显示与当前用户操作有关的内容。从收到 HTML、CSS 和 JavaScript 字节到对其进行必需的处理,从而将它们转变成渲染的像素这一过程中有一些中间步骤,优化性能其实就是了解这些步骤中发生了什么 - 即关键渲染路径。
-
构建对象模型 (DOM & CSSOM)
- HTML 标记转换成文档对象模型 (DOM);CSS 标记转换成 CSS 对象模型 (CSSOM)
- DOM 和 CSSOM 是独立的数据结构
- 转换过程:字节 → 字符 → 令牌 → 节点 → 对象模型
-
渲染树构建、布局及绘制
- DOM 树与 CSSOM 树合并后形成渲染树
- 渲染树只包含渲染网页所需的节点。某些不可见节的节点 (如 head,meta 标签),以及被 CSS 隐藏的标签 (display: none) 将在渲染树中被忽略。有了渲染树后就可以进行布局
- 浏览器完成的步骤:
- 处理 HTML 标记并构建 DOM 树
- 处理 CSS 标记并构建 CSSOM 树
- 将 DOM 与 CSSOM 合并成一个渲染树
- 根据渲染树来布局,以计算每个节点的几何信息
- 将各个节点绘制到屏幕上
- 如果 DOM 或 CSSOM 被修改,您只能再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间
-
“阻塞渲染” 是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。
-
阻塞渲染与 CSS
- HTML 和 CSS 都是阻塞渲染的资源。HTML 的阻塞是理论当然,而目前的网页没有 CSS 几乎没法工作,因此浏览器也将 CSS 作为阻塞资源
- 可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。但即使这些资源不阻塞渲染,浏览器也会下载全部的 CSS
-
阻塞渲染与 JS (未讨论 defer 和 async 的情况)
- JavaScript 可以查询和修改 DOM 与 CSSOM
- 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行
-
优化关键渲染路径的常规步骤如下:
- 对关键路径进行分析和特性描述:资源数、字节数、长度
- 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等
- 优化关键字节数以缩短下载时间(往返次数)
- 优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度
-
PageSpeed 规则和建议