浏览器运行原理

61 阅读13分钟

浏览器内核

浏览器内核是指浏览器用来解析和渲染网页的核心引擎。是浏览器最重要的核心

浏览器内核可以分成两部分:渲染引擎(Rendering Engine)和 JS 引擎。负责解析HTML、CSS、JavaScript 等网页标记语言,并将它们转化为用户可以看到的网页内容。

常见的浏览器内核包括:WebKit、Gecko、Trident、Blink等。不同的浏览器内核对于网页的语法解析会有所不同。

  • IE浏览器(Internet explorer)内核:Trident内核,也是俗称的IE内核;
  • 谷歌浏览器(Chrome)内核:称为Chromium内核或Chrome内核,以前是Webkit内核,现在是Blink内核,为webkit的分支内核;
  • 火狐浏览器(Firefox)内核:Gecko内核,俗称Firefox内核;
  • 苹果浏览器(Safari)内核:Webkit内核, Safari才是Webkit内核的鼻祖;
  • 欧朋浏览器(Opera)内核:最初是自己的Presto内核,后来是Webkit,现在跟Chrome一样是使用webkit的分支内核Blink;

以上便是IT江湖上人称的:五大浏览器,四大内核

市面上常见的浏览器有原生的也有套壳的浏览器:

原生浏览器:拥有完整独立内核的浏览器,如火狐、IE、谷歌(Chrome)、Safari、Opera等。

套壳浏览器:基于开源浏览器内核进行相应功能精简:搜狗、遨游、QQ浏览器内核、360浏览器、猎豹浏览器大多是基于开源内核Chromium。

进程与线程:

进程是cpu资源分配的最小单位(系统会给它分配内存)

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

单线程与多线程,都是指在一个进程内的单和多。

浏览器是多线程

  • 系统给它的进程分配了资源(cpu、内存)
  • 每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
  • 在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)

浏览器优化机制: 每一个Tab标签对应一个进程并不一定是绝对的,打开多个tab页后,有些进程被合并了

浏览器进程:Browser进程(控制进程)、第三方插件进程、GPU进程(图像绘制)、浏览器渲染进程(页面渲染、脚本执行、事件处理)

浏览器的渲染进程是多线程的

CPU GPU

CPU:

  1. CPU是电脑的中央处理器
  2. CPU是一块超大规模的集成电路,其中包含ALU算术逻辑运算单元、Cache高速缓冲存储器以及Bus总线。
  3. CPU是一台计算机的控制和运算核心,它的主要功能便是解释计算机发出的指令以及处理电脑软件中的大数据。
  4. 计算量小, 原理:只有4个运算单元,按顺序计算

GPU:

  1. GPU是电脑的图形处理器
  2. GPU是图像处理器的缩写,它是一种专门为PC或者嵌入式设备进行图像运算工作的微处理器
  3. GPU的工作与上面说过的CPU类似,但又不完全像是,它是专为执行复杂的数学和几何计算而生的.
  4. 计算量大, 原理:有1000个运算单元,可同时计算1000道算术题

CPU是大学教授,做的是高数难题。GPU是一群小学生,做的是普通加减乘除。

GPU就是适合做并行量大的工作。渲染一帧画面,多边形数上百万,互不影响(理想条件)

浏览器内核(Renderer进程)

浏览器的渲染进程是多线程的

  1. GUI渲染线程 (重绘、重排 发生时会触发该线程)
    1. 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    2. 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    3. 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  1. JS引擎线程 (单线程
    1. 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    2. JS引擎线程负责解析Javascript脚本,运行代码。
    3. JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
    4. 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  1. 事件触发线程
    1. 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    2. 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    3. 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    4. 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
  1. 定时触发器线程
    1. 传说中的setInterval与setTimeout所在线程
    2. 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    3. 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
    4. 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  1. 异步http请求线程
    1. 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    2. 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

JS阻塞页面加载

webWorker

  • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
  • 不会影响JS引擎主线程,只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享

SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用

SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程++

浏览器内核:

1、IE浏览器内核:Trident内核,也是俗称的IE内核;

2、Chrome浏览器内核:统称为Chromium内核或Chrome内核,以前是Webkit内核,现在是Blink内核;

3、Firefox浏览器内核:Gecko内核,俗称Firefox内核;

4、Safari浏览器内核:Webkit内核;

5、Opera浏览器内核:最初是自己的Presto内核,后来是Webkit,现在是Blink内核;

6、360浏览器、猎豹浏览器内核:IE+Chrome双内核;

7、搜狗、遨游、QQ浏览器内核:Trident(兼容模式)+Webkit(高速模式);

8、百度浏览器、世界之窗内核:IE内核;

9、2345浏览器内核:以前是IE内核,现在也是IE+Chrome双内核;

浏览器渲染流程

浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

  1. 解析html建立dom树
  2. 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  3. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  4. 绘制render树(paint),绘制页面像素信息
  5. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

渲染完毕后触发load事件,之后就是自己的JS逻辑处理了

load事件与DOMContentLoaded事件的先后

  • 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。(譬如如果有async加载的脚本就不一定完成)
  • 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。(渲染完毕了)

所以,顺序是:DOMContentLoaded -> load

css是由单独的下载线程异步下载的

  • css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
  • 但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息,防止dom的修改)

普通图层和复合图层

浏览器渲染的图层一般包含两大类:普通图层以及复合图层 composite

首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)

其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层。

然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源

(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)

GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒

可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息

如何变成复合图层(硬件加速)

将该元素变成一个复合图层,就是传说中的硬件加速技术

  • 最常用的方式:translate3d、translateZ
  • opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层)作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)
  • <canvas><webgl>等元素
  • 其它,譬如以前的flash插件

absolute和硬件加速的区别

absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。所以,就算absolute中信息改变时不会改变普通文档流中render树,但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。

(浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的)

而硬件加速直接就是在另一个复合层了,所以它的信息改变不会影响默认复合层,只会影响属于自己的复合层,仅仅是引发最后的合成(输出视图)

复合图层的作用:复合图层独立于普通文档流中,改动后可以避免整个页面重绘,提升性能

硬件加速时请使用index

使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染,如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层

Event Loop事件循环

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

  • 主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环
  • 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

定时器线程

为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

当使用setTimeoutsetInterval,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

setTimeout(function(){ console.log('hello!'); }, 1000);

这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

setTimeout(function(){ console.log('hello!'); }, 0); console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:

  • 执行结果是:先begin后hello!
  • 虽然代码的本意是0毫秒后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

宏任务、微任务

宏任务是由宿主发起的,而微任务由JavaScript自身发起

宏任务(macrotask 、task):每次从事件队列中获取一个事件回调并放到执行栈中执行。 在每个宏任务完成之后,下一个宏任务开始之前,对页面进行重新渲染

task->渲染->task->...

微任务(microtask、 job):某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

process.nextTick的优先级高于Promise,在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

  • macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
  • microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护