进程
我们知道,
应用程序能够运行,是需要占用计算机资源的。为了稳定的
并行多个应用程序,计算机必须得做好资源的合理分配和管理,这是操作系统的基本职责。进程便是操作系统进行资源分配的基本单位,是 应用程序 的载体。一个应用程序,又可以是
多进程设计, 比如现代浏览器。
现代浏览器是多进程的
在应用程序中,为了满足功能的需要,主进程会创新新的辅助进程来处理其他任务。这些辅助进程拥有全新的独立的内存空间。如果一个应用程序的这些进程需要通信,可以通过IPC (Inter process Communication )机制来进行。
我们都知道,现代浏览器 都支持多 tab ,一个 tab 对应一个 网页,其实也对应一个网页进程。进程之间互相独立的这种互不影响性 就保证了一个网页的奔溃,不会影响其他网页。当然,如果主进程奔溃,就会影响所有网页了。
不同的浏览器使用不同的架构,下面主要以Chrome为例,主要有 4 个进程:
浏览器主进程(Browser Process): 负责协调、主控,有且只有一个。
- 负责 浏览器界面与用户交互。如前进,后退等;
- 负责各个
tab页面的管理,创建和销毁; - 网络资源下载,文件访问等。
渲染进程(Render Process):也称 渲染引擎,也是我们常说的 浏览器内核。
- 负责 页面渲染,脚本执行,事件处理等
- 它是
多线程的。
插件进程(Plugin Process):负责控制网页使用到的插件。
- 每种插件对应一个进程,仅当使用该插件时才创建。
GPU进程(GPU process):负责所有显示任务。
- 最多一个,用于
3D绘制和硬件加速。
多进程架构的好处
更高的容错性
- 现代
Web(HtmlJsCss) 的复杂性已经越来越高,代码出现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事件