进程
我们知道,
应用程序
能够运行,是需要占用计算机资源
的。为了稳定的
并行
多个应用程序,计算机必须得做好资源的合理分配和管理,这是操作系统
的基本职责。进程
便是操作系统进行资源分配
的基本单位
,是 应用程序 的载体。一个应用程序,又可以是
多进程
设计, 比如现代浏览器
。
现代浏览器是多进程的
在应用程序中,为了满足功能的需要,主进程会创新新的辅助进程来处理其他任务。这些辅助进程拥有全新的独立的内存空间。如果一个应用程序的这些进程需要通信,可以通过IPC (Inter process Communication )
机制来进行。
我们都知道,现代浏览器
都支持多 tab
,一个 tab
对应一个 网页,其实也对应一个网页进程。进程之间互相独立的这种互不影响性 就保证了一个网页的奔溃,不会影响其他网页。当然,如果主进程奔溃,就会影响所有网页了。
不同的浏览器使用不同的架构,下面主要以Chrome
为例,主要有 4 个进程:
浏览器主进程(Browser Process
): 负责协调、主控,有且只有一个。
- 负责 浏览器界面与用户交互。如前进,后退等;
- 负责各个
tab
页面的管理,创建和销毁; - 网络资源下载,文件访问等。
渲染进程(Render Process
):也称 渲染引擎,也是我们常说的 浏览器内核。
- 负责 页面渲染,脚本执行,事件处理等
- 它是
多线程
的。
插件进程(Plugin Process
):负责控制网页使用到的插件。
- 每种插件对应一个进程,仅当使用该插件时才创建。
GPU进程(GPU process
):负责所有显示任务。
- 最多一个,用于
3D
绘制和硬件加速。
多进程架构的好处
更高的容错性
- 现代
Web
(Html
Js
Css
) 的复杂性已经越来越高,代码出现Bug
可能直接导致 渲染引擎 奔溃。 - 多个
tab
页面会开多个 渲染进程,一个页面的崩溃不会影响其他页面的正常运行。
更高的安全性和沙盒性(sanboxing
)
- 网络上一直以来,都充斥着各种恶意代码攻击,甚至会利用一些漏洞安装恶意软件和插件。
- 浏览器多进程的设计,通过对不同进程设置不同的权限,创造 沙盒式 运行环境,使其更安全可靠。
更高的相应速度
- 进程是操作系统进行资源分配的基本单位。
- 单进程设计就意味着,进程中的各个任务会相互竞争,抢夺
CPU
资源。而多进程架构正好改善了这一点,提升了响应速度。
多进程虽然有诸多好处,但是缺点也很明显,那就是 费内存。
为了节约内存,Chorme
浏览器设计和提供了四种 进程模式(Process Models
)
Process-per-site-instance
: 默认;同一个site-instance
使用一个进程。Process-per-site
: 同一个site
使用一个进程。Process-per-tab
: 每个tab
使用一个进程。Single process
: 所有tab
公用一个进程。
那么,site
和 site-instance
的区别是什么呢?
site
: 协议和主域名一致则为 同site
(和 同源策略 不同,不需要 子域名 和 端口号 一致)。site-instance
: connected pages from the same site。包括 同site
页面,还包括由这些页面打开的新页面。即通过<a target='_blank'></a>
和window.open
打开的页面。
Chorme
浏览器默认选择Process-per-site-instance
模式,兼容了性能和易用性,是一种中庸的选择之道。
线程
前面提到,渲染进程又是 多线程 设计,那 线程 又是什么由来呢?
应用程序越来越多,功能迭代使得应用程序对 资源的消耗 也越来越大。
为了提升进程的效率,避免计算机资源的浪费,一个进程又可以有多个
线程
。线程
是CPU进行任务调度的基本单位。可以理解为进程的一个控制单元。一个进程至少有一个线程。所有线程可共享进程所拥有的全部资源。
浏览器内核是多线程的
浏览器内核
主要有以下几个 线程:
GUI线程
DOM
解析,CSS
解析,生成渲染树
。- 当
RenderObject
树需要更新样式
属性时,即发生重绘(Repaint)
。 - 当
RenderObject
树中元素尺寸
,布局
,显示隐藏
等发生变化,即发生回流(reflow)
。 - 在发生
重绘
和回流
时,会形成GUI更新队列
。 GUI线程
和JS引擎线程
互斥。当JS引擎
执行时,GUI线程
会被挂起
。GUI更新队列
只能等到JS引擎
空闲时被执行。
JS引擎线程
(比如 V8
)
- 也称
JS内核
,负责解析Javascript
脚本,运行代码。 GUI渲染线程
与JS引擎线程
是互斥
的。所以如果JS
执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程
- 用来控制事件循环(
Event Loop
)。 - 配合
JS引擎
处理异步代码
。JS
异步代码的回调函数
会形成一个任务队列
,等待JS引擎
空闲时被调用。
定时触发器线程
setInterval
与setTimeout
所在线程(因为JS引擎
是单线程
的, 如果处于阻塞线程
状态就会影响记计时的准确)。定时器事件
会在计时完毕后,添加到事件队列
中,等待JS引擎
空闲后执行。
异步http请求线程
- 在
XMLHttpRequest
在连接
后是通过浏览器新开一个异步http请求线程
来请求。 - 将检测到状态变更时,如果设置有回调函数,异步线程就产生
状态变更事件
,将这个回调再放入事件队列
中。等待JS引擎
空闲后执行。
浏览器渲染流程
主要任务
浏览器渲染流程的主要任务如下(不一定按顺序):
html
文档解析
- 遇到
JS
脚本,会加载并执行。 - 遇到
CSS
代码,同步进行解析。 - 过程中可能会被
CSS
和JS
的加载而执行阻塞 - 解析完成,生成
Dom
树。 - 在控制台
console
里面输入document
可查看Dom
树.
CSS
解析
html
解析过程中,会对遇到的CSS
同步进行解析:CSS
包括: 标签自带样式;通过link/@import
引用的样式文件;style
标签内的样式;元素内嵌的样式。- 转换样式表中的属性值,使其标准化,比如
em
转px
,bold
转换成一个具体的数值,red
转换城rgb(255,0,0)
等。 - 计算每个
DOM
的具体样式。 CSS
继承。
不会阻塞
html
解析流程。解析完成,生成
CSSOM(CSS Object Model)
。在控制台
console
里面输入document.styleSheets
可查看CSSOM
。
JS
脚本执行
html
解析过程中,会对遇到的JS
脚本进行加载并执行。CSS
解析与JS
脚本的执行互斥(JS
可能会等待CSSOM
生成以后执行)。Webkit
内核中进行了JS
执行优化,只有在JS
访问CSS
时才会发生互斥。
Dom树
和 CSSOM
结合生成 渲染树(Rendering Tree
)
- 和
Dom树
节点不是完全对应的。 - 比如
head
标签下的所有内容,display:one
的元素就不会被添加到渲染树中。 visibility: hidden
的元素在渲染树中。- 渲染树是一系列将被渲染的对象。
布局(layout
):
计算渲染树各节点元素的尺寸、位置等。
这里的计算实际上是“三维”的计算,分层布局(这里涉及
复合图层
和硬件加速
的概念)。布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。
打开
Chrome
控制台,输入command+shift+p
,选择show Rendering
, 选择Layer borders
中看到,黄色的就是复合图层。
绘制(paint
):绘制页面像素信息。
- 浏览器会遍历渲染树,调用渲染器的
paint()
方法在屏幕上显示其内容。 - 渲染树的绘制工作是浏览器通过将各层的信息发送给
GPU
,GPU
会进行合成(composite
),显示在屏幕上。 - 打开
chrome
控制台,输入command+shift+p
,选择show Layers
,可以查看图层。
回流 和 重绘
回流(Reflow)
: 基于渲染树的页面布局,是一种流式布局。当页面元素修改,引起布局的变化,浏览器就会从html
这个 root
根结点 自上而下遍历,进行重新计算渲染。引发布局变化的操作主要有:
- 页面第一次渲染(初始化)
DOM
树变化(如:增删节点)Render
树变化(如:显示隐藏,位置,大小等修改)- 浏览器窗口
resize
重绘(Repaint)
: 页面元素改变的时候,浏览器会对涉及内容进行重画。
- 回流必定引起重绘。
- 仅元素样式改变,不影响布局的情况下,重绘可单独触发。
相关事件
domContentLoaded
:当html
解析完成,生成Dom树
后触发。
- 此时
Dom
元素可以被访问。 - 此时
document.readystate
从loading
变成interactive
onload
事件触发: 页面资源全部已加载完成后触发。
- 此时 页面上所有的
DOM
,样式表,脚本,图片都已经加载完成。 - 此时
document.readystate
从interactive
变成completed
异步脚本 和 延迟脚本
异步脚本: 带async
的脚本
HTML
还没有被解析完的时候,async
脚本已经加载完了,那么HTML
停止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded
事件。HTML
解析完了之后,async
脚本才加载完,然后再执行脚本,那么在HTML
解析完毕、async
脚本还没加载完的时候就触发DOMContentLoaded
事件。- 一定会在
load
事件之前执行。
延迟脚本: 带 defer
的脚本
- 不会影响
HTML
文档的解析,而是等到HTML
解析完成后才会执行。 - 肯定在
defer
脚本执行结束后,DOMContentLoaded
才会被触发。
渲染阻塞
JS阻塞页面
: JS
可能操作和修改DOM
,也可能操作CSSOM
来修改节点样式,所以浏览器在遇到<script>
标签时,DOM
构建将暂停,直至脚本完成执行(外部脚本还要先下载完成再执行),然后继续构建DOM
。浏览器甚至会延迟脚本执行和构建DOM
,直至完成其CSSOM
的下载和构建。这就是所谓的JS
阻塞页面。
- 现在可以在
script
标签上增加属性defer
或者async
来改善。 - 浏览器会将脚本中改变
DOM
和CSS
的地方分别解析出来,追加到DOM
树和CSSOM
规则树上。 - 所以,
script
标签的位置很重要。
CSS阻塞渲染
: 页面是通过渲染树绘制的,渲染树的生成又必须等所有的CSS
(内联、内部和外部)都已经下载完,并解析完生成CSSDOM
。这就是CSS
阻塞渲染。
CSS
阻塞渲染意味着,在CSSOM
完备前,页面将一直处理白屏状态,这就是为什么样式放在head
中,仅仅是为了更早的解析CSS
,保证更快的首次渲染。- 需要注意的是,即便你没有给页面任何的样式声明,
CSSOM
依然会生成,默认生成的CSSOM
自带浏览器默认样式。
优化建议
关键是要深入理解浏览器渲染流程,合法的书写 HTML
,CSS
,JS
。
减少JS
阻塞
- 合理运用
defer
或者async
。 - 在
domContentLoaded
或者onload
事件触发时,通过动态创建script
标签的方式引入js
文件。 - 合理运用缓存策略。
- 要避免过多
JS
请求,也要避免单个JS
过大。
减少CSS
阻塞
- 样式尽量前置(放
head
标签),尽早解析生成CSSDOM
。 - 样式精简,嵌套层级最小化。
减少重绘
- 合法,合理书写
Html
布局是基础,避免无用的深层嵌套。 - 减少 或者 合并
Dom
操作,读/写操作尽量放一起。 - 通过
document
对象的createDocumentFragment()``cloneNode()
方法创建离线Dom
,完成操作后再用于真实Dom
。 window.requestAnimationFrame()
进行动画优化。
参考资料
从浏览器多进程到JS单线程
setTimeout和requestAnimationFrame
浏览器渲染原理与过程
DOMContentLoaded 与 load事件