webkit理论知识

浏览器进程与线程

进程

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

  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的准备进程、

进程模型有如下特征:html

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

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

浏览器开启的进程

线程

每个进程内部,都有不少线程。web

多线程模型

Browser进程下有不少线程:
Xnip2020-01-16_16-30-30.jpgchrome

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

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元素的最后。

建议1、

<html>
<head>
    <script type="" async>
    ...
    </script>
</head>
<body>
    <img src="" />
</body>
<html>

建议2、

<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绘制每一帧所须要的时间:1、使用合适的网页分层技术以减小须要从新计算的布局和绘图;2、使用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技术内幕》
  • 极客时间 《浏览器工做原理与实践》
相关文章
相关标签/搜索