浏览器渲染
进程 (process) 和线程 (thread)
进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
线程是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。
对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动了一个浏览器进程,打开一个 Word 就启动了一个 Word 进程。
在一个进程内部,要同时做多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程。
系统会给每个进程分配独立的内存,因此进程有它独立的资源。同一个进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。
async
可选属性。只对外部脚本文件有效。
首先我们要知道,对于普通的引用外部脚本文件的<script>
标签,当浏览器解析到这个标签的时候会怎么做呢?
<script>
标签阻塞了浏览器对 HTML 的解析,如果获取 JS 脚本的网络请求迟迟得不到响应,或者 JS 脚本执行时间过长,都会导致白屏,用户看不到页面内容<script>
时,它又会怎么做呢?如果在脚本请求完成之前,HTML 已经解析完毕了,那就会立即执行 JS 脚本代码
正因为如此,异步脚本不应该在加载期间修改DOM。 异步脚本保证会在页面的load
事件之前执行,但可能会在DOMContentLoaded
(下文中解释)之前或之后
defer
可选属性。也只对外部脚本文件有效。
当浏览器遇到带有 defer 属性的<script>
时,获取该脚本的网络请求也是异步的,不会阻塞浏览器解析 HTML,一旦网络请求完成之后,如果此时 HTML 还没有解析完,浏览器不会暂停解析HTML,而是等待 HTML 解析完毕再执行 JS 代码。
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script defer src="example1.js"></script>
<script defer src="example2.js"></script>
</head>
<body>
<!-- 这里是页面内容 -->
</body>
</html>
HTML5规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚本会在第二个推迟的脚本之前执行,而且两者都会在DOMContentLoaded
事件之前执行.
defer和async的区别:
两者的script网络请求与hmtl的解析都是异步的
defer在script请求完后等待html解析完再解析
async则会暂停html的解析
DOMContentLoaded与load
DOMContentLoaded:当纯HTML被完全加载以及解析时,DOMContentLoaded
事件会被触发,而不必等待样式表,图片或者子框架完成加载。也就是只要页面DOM加载完成就触发,无需等待依赖资源的加载。
load:当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load
事件
同步script与 DOMContentLoaded:
(1)既无js也无css的情况下,HTML文档的解析过程:
(2)有css无js的情况下,HTML文档的解析过程:
DOMContentLoaded
触发时间仍为DOM之后,无论此时CSS解析为CSSOM的过程是否完成。
(3)既有css也有js的情况下,HTML文档的解析过程:
综上,在只有同步的script标签代码中,DOMContentLoaded
执行时间如下:
带async的script 与 DOMContentLoaded:
- HTML 还没有被解析完的时候,async脚本已经加载完了,那么 HTML 停止解析,去执行脚本,脚本执行完毕后触发
DOMContentLoaded
事件。
- HTML 解析完了之后,async脚本才加载完,然后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发
DOMContentLoaded
事件。
带defer的script 与 DOMContentLoaded:
defer的script脚本在html解析完后再执行,执行完后再调用DOMCONTENTLOADED
浏览器的多进程架构
以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能
优点
默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。
同样,第三方插件崩溃也不会影响到整个浏览器。
浏览器的主要进程和职责
主进程 Browser Process
负责浏览器界面的显示与交互。各个页面的管理,创建和销毁其他进程。网络的资源管理、下载等
第三方插件进程 Plugin Process
每种类型的插件对应一个进程,仅当使用该插件时才创建。
GPU 进程 GPU Process
该进程也只有一个,用于3D/动画绘制等等
渲染进程 Renderer Process
称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。 (本文重点分析)
浏览器的渲染进程是多线程的,我们来看看它有哪些主要线程 :
1. GUI 渲染线程
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
GUI渲染线程与JS引擎线程是互斥的
当JS引擎执行时GUI线程会被挂起(相当于被冻结了)
GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
2. JS 引擎线程
Javascript 引擎,也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)
JS 引擎线程负责解析 Javascript 脚本,运行代码。
JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。
注意,GUI 渲染线程与 JS 引擎线程是互斥的,
例如浏览器渲染的时候遇到script
标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况
3. 事件触发线程
归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助)
当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)
4. 定时触发器线程
传说中的 setInterval 与 setTimeout 所在线程
浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
5. 异步 http 请求线程
在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求。
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。
浏览器渲染流程
解析HTML构建DOM树
Bytes(字节) -> Characters(字符) -> Tokens(词) -> Nodes(节点) -> DOM(DOM树)
解析CSS构建CSSOM树
解析JavaScript脚本
JavaScript
引擎运行完毕,浏览器才会从中断的地方恢复DOM树的构建为什么上面也说了,JS会对DOM节点进行操作,浏览器无法预测未来的DOM节点的具体内容,为了防止无效操作,节省资源,只能阻塞DOM树的构建
若在 HTML 头部加载 JS 文件,由于 JS 阻塞,会推迟页面的首绘,所以为了加快页面渲染,一般将 JS 文件放到HTML 底部进行加载,或是对 JS 文件执行 async
或 defer
加载
async
是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 onload
前,但不确定在 DOMContentLoaded
事件的前或后
defer
是延迟执行,在浏览器看起来的效果像是将脚本放在了 body
后面一样(虽然按规范应该是在 DOMContentLoaded
事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)
构建渲染树/呈现树(Render Tree)
渲染树 ( Render Tree ) 由 DOM树
、CSSOM树
合并而成,但并不是必须等 DOM树
及 CSSOM树
加载完成后才开始合并构建 渲染树
,三者的构建并无先后条件,也并非完全独立,而是会有交叉,并行构建
CSSOM 树
和 DOM 树
合并成渲染树,渲染树
只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上
如上图 ( 网图侵删 ) ,为了构建渲染树,我们看看浏览器都做了什么
浏览器首先会从DOM树的根节点开始遍历每个可见节点
例如脚本标记、元标记等有些节点不可见,因为它们不会体现在渲染输出中,所以会被忽略
某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如上图的其中一个 span
标签有 display: none
属性,也会被忽略
对于每个可见节点,找到其对应的的 CSSOM 规则并应用它们
输出可见节点,连同其内容和计算的样式
布局(Layout)
渲染树
同时包含了屏幕上的所有可见内容及其样式信息,但我们还没有计算它们在设备 视口 内的确切位置和大小,这就是 布局
( Layout ) 阶段,布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算,也称为 自动重排
或 回流
( Reflow )
绘制(Painting)
经由前几步我们知道了哪些节点可见、它们的计算样式以及几何信息,我们将这些信息传递给最后一个阶段将渲染树中的每个节点转换成屏幕上的实际像素,也就是俗称的 绘制
或 栅格化
绘制
过程中有一种绘制叫 重绘
重绘(Repaint)
元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了,这叫做 重绘
( Repaint )
回流 (Reflow)
回流
一定伴随着 重绘
,重绘
却可以单独出现
合成(Composite)
先来总结一下上面的步骤,到目前我们经历渲染过程如下:
首先解析 HTML
文档,形成 DOM 树
接着解析 CSS
,产生 CSSOM树
在DOM和CSSOM树解析过程中,遇到 JS,会立即阻塞DOM树的构建,JS解析完成,接着走上面两步
再接着,浏览器通过DOM和CSSOM树构建渲染树 ( Render树 )
这个过程中,DOM中不可见标签元素不会放到渲染树中,就像<head></head> 或 display:none
CSSOM树规则会附加给渲染树的每个元素上
渲染树构建完成,浏览器会对这些元素进行定位和布局,这一步也叫 重排/回流 ( Reflow
) 或 布局(Layout
)
接下来绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做 重绘 (Repaint
)
再接下来是我们这最后一步合成( composite
),浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上
- 构建DOM树,即创建document对象,解析html元素和字符数据,添加element节点和text节点到docunment中,此时,document.readyState=‘loading’
- 遇到link外部CSS,创建线程加载,并继续解析文档
- 遇到script外部JS:
a.未设置async、defer,GUI渲染线程阻塞,等待JS加载并执行完成,
b.设置async或defer,加载完立即执行或文档解析完后执行
- 遇到img等,异步加载src,继续解析文档,CSSOM和DOM的构建是并行的
- 文档解析完成,document.readyState=‘interactive’
- 设置有defer的脚本执行
- document对象除法DOMContentLoaded事件,文档解析完成
- 文档和所以资源加载完成,触法onload事件
- 此后,以异步响应方式处理用户输入、网络事件
题解
1. 为什么 Javascript 要是单线程的 ?
如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。
如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,
假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。
2. 为什么 JS 阻塞页面加载 ?
JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
3. css 加载会造成阻塞吗 ?
1.DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析。
2.但Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
所以他(DOM)必须等待CSSOM Tree构建完成,也就是CSS资源加载完成后才能开始渲染。
因此,CSS加载会阻塞DOM的渲染
3.由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
因此,样式表会在后面的 js 执行前先加载执行完毕,所以css 会阻塞后面 js 的执行。
4. DOMContentLoaded 与 load 的区别 ?
当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片等资源已经加载完毕
DOMContentLoaded -> load。
5. 什么是 CRP,即关键渲染路径(Critical Rendering Path)?如何优化 ?
关键渲染路径是浏览器将 HTML CSS JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是我们上面说的浏览器渲染流程。
优化 JavaScript
当浏览器遇到 script 标记时,会阻止解析器继续操作,直到 CSSOM 构建完毕(JavaScript引擎和GUI线程互斥),JavaScript 才会运行并继续完成 DOM 构建过程。
async: 当我们在 script 标记添加 async 属性以后,浏览器遇到这个 script 标记时会继续解析 DOM,同时脚本也不会被 CSSOM 阻止,即不会阻止 CRP,。
defer: 与 async 的区别在于,脚本需要等到文档解析后( DOMContentLoaded 事件前)执行,(两者下载的过程不会阻塞 DOM,但执行会)。
6.浏览器的回流与重绘
回流必将引起重绘,重绘不一定会引起回流。
回流(Reflow)
当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
页面首次渲染
浏览器窗口大小发生改变
元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
元素字体大小变化
添加或者删除可见的 DOM 元素
重绘(Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
参考
JavaScript高级程序设计第四版
阮一峰老师的ES6教程
DOMContentLoaded
图解script标签中的async和defer属性