1

浏览器进程与线程

进程

浏览器是多进程模型。chrome浏览器主要包括以下进程类型

  1. Brower进程:浏览器的主进程,负责浏览器界面的显示,各个页面的管理,是所有其他类型进程的祖先,主要负责他们的创建和销毁工作,它有且仅有一个
  2. Render进程:网页的渲染进程,负责页面的渲染工作。Renderer进程的数量是不固定的,各个浏览器可以有不同的配置。默认情况下为process-per-tab,即为每一个标签页创建一个独立的进程,而不管它们是否是不同域不同实例。我们使用的chrome浏览器默认一个标签对应一个进程,但是如果是从一个页面打开了新页面,而新页面和当前页面属于同一个站点时,那么新页面会复用父页面的进程。(同一站点定义为根域名加上协议一致即为同一个站点)。为什么要共用一个渲染进程呢?因为他们会共享JS的执行环境,例如新页面可以使用window.opener.location.href=“”控制父页面的链接。除非在打开新页面的时候使用了rel="noopener noreferrer",此时会是独立的进程,新页面也拿不到window.opener了
  3. NPAPI插件进程:该进程是为NPAPI类型的插件而创建的。其创建的原则是每种类型的插件只会被创建一次,而且仅当使用时才会被创建。当有多个网页需要使用同一种类型的插件时,进程会为每一个使用者创建一个实例,插件进程是被共享的。
  4. GPU进程:最多只有一个,而且仅当GPU硬件加速打开时会被创建,主要是对3D图形加速调用的实现。
  5. Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程
  6. 其它类型的进程:例如Linux下的Zygote进程,另外就是Sandbox的准备进程、

进程模型有以下特征:

  1. Brower进程和页面的渲染是分开的,这保证了页面渲染导致的崩溃不会导致浏览器主界面的崩溃
  2. 每个网页是独立的进程,这保证了页面之间相互不影响
  3. 插件进程也是独立的,插件本身的问题不会影响浏览器主界面和网页
  4. GPU硬件加速进程也是独立的。

通过chrome浏览器右上角的三个点--More Tools--Task Manager可以查看当前浏览器所开启的进程。注:三个tab共享一个进程的情况是在第一个Tab打开了另外两个tab

浏览器开启的进程

线程

每一个进程内部,都有很多线程。

多线程模型

Browser进程下有很多线程:
Xnip2020-01-16_16-30-30.jpg

其中线程1 Chrome是主线程。Chrome IOThread线程就是IO线程。中间还有用来处理视频、存储、王阔、文件、音频、浏览历史等的线程。

Render进程下有以下线程
render进程所包含的线程

其中线程Chrome是主线程,Chrome IOThread线程就是IO线程。线程2是一个新的线程,用来解释HTML文档。

网页的加载和渲染过程的基本工作方式如下:

  1. Browser进程收到用户的请求,首先由UI线程处理,而且将相应的任务转达给IO线程,它随即将该任务传递给Renderer进程。
  2. Renderer进程的IO线程经过简单解释后交给渲染线程。渲染线程接受请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染。最后Render进程将结果由IO线程传递给Browser进程。
  3. 最后,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技术内幕》
  • 极客时间 《浏览器工作原理与实践》

深蓝一人
1.6k 声望65 粉丝

暂时没有个人简介


« 上一篇
React Events
下一篇 »
redux原理