浏览器进程与线程
进程
浏览器是多进程模型。chrome浏览器主要包括以下进程类型
- Brower进程:浏览器的主进程,负责浏览器界面的显示,各个页面的管理,是所有其他类型进程的祖先,主要负责他们的创建和销毁工作,它有且仅有一个
- Render进程:网页的渲染进程,负责页面的渲染工作。Renderer进程的数量是不固定的,各个浏览器可以有不同的配置。默认情况下为process-per-tab,即为每一个标签页创建一个独立的进程,而不管它们是否是不同域不同实例。我们使用的chrome浏览器默认一个标签对应一个进程,但是如果是从一个页面打开了新页面,而新页面和当前页面属于同一个站点时,那么新页面会复用父页面的进程。(同一站点定义为根域名加上协议一致即为同一个站点)。为什么要共用一个渲染进程呢?因为他们会共享JS的执行环境,例如新页面可以使用window.opener.location.href=“”控制父页面的链接。除非在打开新页面的时候使用了rel="noopener noreferrer",此时会是独立的进程,新页面也拿不到window.opener了
- NPAPI插件进程:该进程是为NPAPI类型的插件而创建的。其创建的原则是每种类型的插件只会被创建一次,而且仅当使用时才会被创建。当有多个网页需要使用同一种类型的插件时,进程会为每一个使用者创建一个实例,插件进程是被共享的。
- GPU进程:最多只有一个,而且仅当GPU硬件加速打开时会被创建,主要是对3D图形加速调用的实现。
- Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程
- 其它类型的进程:例如Linux下的Zygote进程,另外就是Sandbox的准备进程、
进程模型有以下特征:
- Brower进程和页面的渲染是分开的,这保证了页面渲染导致的崩溃不会导致浏览器主界面的崩溃
- 每个网页是独立的进程,这保证了页面之间相互不影响
- 插件进程也是独立的,插件本身的问题不会影响浏览器主界面和网页
- GPU硬件加速进程也是独立的。
通过chrome浏览器右上角的三个点--More Tools--Task Manager可以查看当前浏览器所开启的进程。注:三个tab共享一个进程的情况是在第一个Tab打开了另外两个tab
线程
每一个进程内部,都有很多线程。
Browser进程下有很多线程:
其中线程1 Chrome是主线程。Chrome IOThread线程就是IO线程。中间还有用来处理视频、存储、王阔、文件、音频、浏览历史等的线程。
Render进程下有以下线程
其中线程Chrome是主线程,Chrome IOThread线程就是IO线程。线程2是一个新的线程,用来解释HTML文档。
网页的加载和渲染过程的基本工作方式如下:
- Browser进程收到用户的请求,首先由UI线程处理,而且将相应的任务转达给IO线程,它随即将该任务传递给Renderer进程。
- Renderer进程的IO线程经过简单解释后交给渲染线程。渲染线程接受请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染。最后Render进程将结果由IO线程传递给Browser进程。
- 最后,Browser进程接受到结果并绘制
浏览器的资源加载
HTML支持的资源大致有:Html/Js/CSS/图片/svg/视频、音频等,资源在加载过程中分为在缓存中和不在缓存中两种情况。
例如在解析Html过程中,发现一个img标签,webkit会专门创建一个ImageLoader去加载该资源。由于获取资源耗时较长,通常是异步执行的,也就是说资源的获取和加载不会阻碍当前Webkit的渲染过程,例如图片/CSS。
当然某些资源例如JS会阻碍主线程的渲染。Webkit会怎么做呢?当前的主线程被阻碍时,webkit会另起一个线程去遍历后面的HTML网页,收集需要的资源URL,发送请求。这样就可以避免被阻碍。与此同时,Webkit能够并发下载这些资源,甚至并发下载JS代码资源,这种机制对于网页的加速加载很是明显。
这个地方说法跟通常讲的将script标签加上async属性或者放在body结束标签的前面来提升性能的说法不太一致。书中给出的解释是,就算webkit有自己的优化策略,但还是建议加上async属性或者放在body结束标签的前面,因为并不是所有的渲染引擎都作了如此的考虑
缓存相关
缓存资源池是有限的,必须有响应的机制来替换其中的资源,这个机制就是LRU(Last Recent Used)最少使用原则。
网络请求中,DNS解析和TCP连接占用大量的时间。网页开发者可以从以下方面着手减少这一部分时间
- 减少链接重定向
- 利用DNS预解析
<Link rel="dns-prefetch" href="...">
- 搭建支持SPDY协议的服务器
- 避免错误的链接请求,失效的链接也会占用网络资源
减少资源的数量
- 内嵌小型的资源,例如JS和CSS,减少网络请求。图片转为base64的等
- 合并资源,利用雪碧图等
- 利用浏览器缓存
Html解析器
在Render进程中有一个线程,该线程用来处理HTML文档的解释任务。因为JS代码可能会修改文档结构,所以JS代码的执行会阻塞后面节点的创建,同时也会阻碍后面的资源下载。所以有两点建议
- 将script标签加上async属性,表明该脚本可以异步执行
- 将script标签放在body元素的最后。
建议一、
<html>
<head>
<script type="" async>
...
</script>
</head>
<body>
<img src="" />
</body>
<html>
建议二、
<html>
<head>
</head>
<body>
<img src="" />
<script type="">
...
</script>
</body>
<html>
但其实在执行JS代码时,webkit有自己的优化机制,webkit会先暂停JS执行,扫描后面的词语,如果发现有其它资源,使用预资源加载器来发送请求,在这之后才执行JS代码。尽管如此,还是推荐按建议的写代码,毕竟不是所有的渲染引擎都做了考虑。
事件机制
事件分为3个阶段,1事件捕获阶段,2处于目标事件阶段,3冒泡阶段,可以使用event.phase获取当前所属的阶段。使用addEventListner默认是在冒泡阶段捕获事件,除非最后一个参数制定个为true;
大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,可以最大程度兼容各种浏览器。最好只在需要在事件到达目标之前捕获它时才添加到捕获阶段。
<html>
<body id="body">
<div id="div">
<span id="span">span元素</span>
</div>
<script type="text/javascript">
function onSpan(event) {
console.log('on span');
}
function onDiv(event) {
console.log('on div');
}
function onBody(event) {
console.log('on body');
}
window.onload = function () {
const spanEle = document.getElementById('span');
spanEle.addEventListener('click', onSpan);
const divEle = document.getElementById('div');
divEle.addEventListener('click', onDiv);
const bodyEle = document.getElementById('body');
bodyEle.addEventListener('click', onBody, true);
}
</script>
</body>
</html>
点击span后代码执行顺序为 body、 span、 div
浏览器渲染
在chrome的console中使用document查看当前页面的DOM树结构。当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。在控制台中输入 document.styleSheets就可以看到对应的结构。总结如下
- 浏览器不能直接理解 HTML 数据,所以第一步需要将其转换为浏览器能够理解的 DOM 树结构;
- 生成 DOM 树后,还需要根据 CSS 样式表,来计算出 DOM 树所有节点的样式;
- 最后计算 DOM 元素的布局信息,使其都保存在布局树中。
网页层次
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动、video节点,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
具体以下情况会生成单独的合成层:
- 具有CSS 3D属性或者CSS透视效果
- 节点是使用硬件加速的视频解码技术的HTML5 video元素
- 节点是使用硬件加速的canvas 2D元素或者WebGL技术
- 使用了硬件加速的CSS filters技术
- 使用了剪裁(Clip)或者反射(Reflection)属性,并且后代中包含一个合成层
- 有一个Z坐标比自己小的兄弟节点,并且该节点是一个合成层
每个RenderLayer对象可以被想象成图像中的一个层,各个层一同构成了一个图像,在渲染过程中,每个层对应网页中的一个或者一些可视元素,这些元素绘制内容到该层上,把这个过程称为绘图操作。如果绘图操作需要GPU完成,称之为GPU硬件加速绘图。理想情况下,每个层都有个绘制的存储区域,这个存储区域用来保存绘图的结果。最后,需要将这些层的内容合并到同一个图像中,称之为合成。
网页分层有两个原因:一是为了方便网页开发者开发网页并设置网页的层次;二是为了webkit处理上的遍历,也就是为了简化渲染逻辑。
从输入网页URL到构建完DOM树这个过程:
- 当用户输入URL的时候,Webkit调用其资源加速器加载该URL对应的网页
- 加载器依赖网络模块建立连接,发送情感求并接收答复
- Webkit接收到各种网页或者资源的数据,其中某些资源可能是同步或异步的
- 网页被交给HTML解释器转变成一系列的词语
- 解释器根据词语构建节点,形成DOM树
- 如果节点是JS代码的话,调用JS引擎解释并执行
- JS代码可能会修改DOM树的结构
- 如果节点需要依赖其他资源,例如图片、CSS、视频等,调用资源加载器来加载它们,但是它们是异步的,不会阻碍当前DOM树的继续构建,如果是JS资源URL(没有标记异步方式),则需要停止当前DOM树的构建,直到JS资源被加载并被JS引擎执行后继续DOM树的构建
接下来就是Webkit利用CSS和DOM树构建RenderObject树直到绘图上下文,具体过程如下:
- CSS文件被CSS解释器解释成内部表示结构
- CSS解释器工作完之后,在DOM树上附加解释后的样式信息,这就是RenderObject树
- RenderObject节点在创建的同时,Webkit会根据网页的层次结构创建RenderLayer树,同时构建一个虚拟的绘图上下文。
- 最后就是根据绘图上下文来生成最终的图像,这一过程主要依赖2D和3D图形库。这一过程还会涉及到GPU硬件渲染、混合渲染模型等方式
再看重绘、重排和合成
网页加载后,每当重新绘制新的一帧时,一般需要经过三个阶段:计算布局--绘图--合成。其中前两个阶段比较耗时间,合成时间相对要少一些。在实际应用中,可以通过如下方法来减少webkit绘制每一帧所需要的时间:一、使用合适的网页分层技术以减少需要重新计算的布局和绘图;二、使用CSS 3D变形和动画技术。
- 重绘:更改元素的尺寸,例如元素的宽高,那么浏览器会触发计算布局、绘图和合成三个阶段。
- 重排:更改元素的背景色,没有几何尺寸改变,会省去构建RenderObject树和RenderLayer树阶段,直接到绘制、合成阶段
- 合成:例如使用了 CSS 的 transform 来实现动画效果,会直接到最后一步的合成阶段
JS引擎
为什么说JS效率低?
JS语言的一个特点是它是无类型语言,没有办法在编译的时候知道变量类型,运行的时候才能确定。在运行时计算和决定类型,会带来很严重的性能损耗。
这相较于静态语言例如C++的区别是,静态语言只需要知道变量的地址及类型,地址加上类型的长度,就可以得出该变量的值。
- 编译确定位置:C++有明确的两个阶段,编译这些位置的偏移信息都是编译器在编译阶段就决定了的。当C++代码编译成本地代码之后,对象的属性和偏移信息都计算完成。而JS没有类型,只有在对象创建的时候才有这些信息,因而只能在执行阶段确定。
- 偏移信息共享:C++因为有类型定义,所以所有对象都是该类型来确定的,而且执行的时候不能动态改变类型。所以访问它们只需要按照编译时确定的偏移量即可。JS则不同,每个对象都是自描述,属性和偏移位置信息都包含在自身结构中。
- 偏移信息查找:C++中查找偏移地址很简单,都是在编译代码时,对使用到的某类型成员变量直接设置偏移量。而JS代码使用到一个对象则需要通过属性名匹配才能找到对应的值
JS的编译和执行
JS引擎就是能够将JS代码处理并执行的运行环境。JS引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段。
语法分析
语法分析就是通过词法分析和语法分析得到语法树的过程,如果在构造语法树的时候,发现错误,就会报错并结束整个代码块的解析。
预编译阶段
首先了解变量对象(Variable Object, 缩写为VO)是用于存储执行上下文中的:
- 变量
- 函数声明
- 函数参数
在函数上下文中,变量对象被表示为活动对象AO;
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
b=20;
}
test(10)
VO按照如下顺序填充:
- 函数参数(若未传⼊入,初始化该参数值为undefined)
- 函数声明(若发⽣生命名冲突,会覆盖)
- 变量声明(初始化变量值为undefined,若发⽣生命名冲突,会忽略。
因此
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <ref to func "d"\>
e: undefined
};
代码执行阶段
AO(test) = {
a: 10,
b: 20,
c: 10,
d: <reference to FunctionDeclaration "d"\>
e: function \_e() {};
};
参考文档
- 《webkit技术内幕》
- 极客时间 《浏览器工作原理与实践》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。