1 浏览器结构
浏览器分为以下7个部分:
- 用户界面
- 浏览器引擎:在用户界面和呈现引擎之间传送指令。
- 渲染引擎:也称为呈现引擎/浏览器内核,负责显示请求的内容。
不同的浏览器有不同的呈现引擎,例如:
Chrome: Blink。Blink是Webkit的一个分支,添加了一些优化新特性。
Safari: Webkit
Firfox: Gecko
IE: Trident
- 网络:用于网络调用,比如 HTTP 请求。
- 用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。
- javascript 解释器:用于解析和执行 JavaScript 代码。又称为 JavaScript 引擎,也可以成为 JavaScript 内核,在线程方面又称为 JavaScript 引擎线程。
同样不同的浏览器有不同的JS解释器,例如:
Chrome: V8引擎。之前是机器码,最近重回字典码
Safari: JavaScriptCore引擎
Firfox: Ion引擎
PS:不同浏览器间引擎性能差距不大,详见https://arewefastyet.com
- 数据存储:这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。
2 浏览器占比
2.1 PC浏览器占比
2018年2月份占比
2.2 移动浏览器占比
3 渲染引擎详解
浏览器渲染引擎最重要的工作就是将 html 和 CSS 文档解析组合最终渲染到浏览器窗口上。如下图所示:
解析 HTML 构建 DOM 树时渲染引擎会将 HTML 文件的便签元素解析成多个 DOM 元素对象节点,并且将这些节点根据父子关系组成一个树结构。同时 CSS 文件被解析成 CSS 规则表,然后将每条 CSS 规则按照「从右向左」的方式在 DOM 树上进行逆向匹配,生成一个具有样式规则描述的 DOM 渲染树。接下来就是将渲染树进行布局、绘制的过程。首先根据 DOM 渲染树上的样式规则,对 DOM 元素进行大小和位置的定位,关键属性如position;width;margin;padding;top;border;...,接下来再根据元素样式规则中的color;background;shadow;...规则进行绘制。另外,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
再者,需要注意的是,在浏览器渲染完首屏页面后,如果对 DOM 进行操作会引起浏览器引擎对 DOM 渲染树的重新布局和重新绘制,我们叫做「重排」和「重绘」,由于重排和重绘是前后依赖的关系,重绘发生时未必会触发渲染引擎的重排,但是如果发生了重排就必然会触发重绘操作,这样带来的性能损害就是巨大的。因此我们在做性能优化的时候应该遵循「避免重排;减少重绘」的原则。[1]
4 浏览器的进程与线程
4.1 浏览器的进程
在浏览器刚被设计出来的时候,那时的网页非常的简单,每个网页的资源占有率是非常低的,因此一个进程处理多个网页时可行的。然后在今天,大量网页变得日益复杂。把所有网页都放进一个进程的浏览器面临在健壮性,响应速度,安全性方面的挑战。因为如果浏览器中的一个tab网页崩溃的话,将会导致其他被打开的网页应用。另外相对于线程,进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。
因此Chrome浏览器针对每一个tab页签,都开了一个进程。
4.2 浏览器的线程
浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器大概会有以下几个线程:JavaScript引擎线程,GUI渲染线程(又称为UI主线程),浏览器事件触发线程,异步 HTTP 请求线程(Chrome最多并发6个)。
JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。
var isEnd = true;
window.setTimeout(function () {
isEnd = false;//1s后,改变isEnd的值
}, 1000);
//这个while永远的占用了js线程,所以setTimeout里面的函数永远不会执行
while (isEnd);
GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。但需要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时立即被执行。
<script>
btn.onclick=function() {
div.style.height="900px"; //为什么不先到900然后在到100px?
div.style.height="100px";
}
</script>
事件触发线程,当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系,所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。[2]
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
console.log(3) // 输出结果1,3,2
4.3 开启额外的线程-Web Worker
如下图,耗时JS的同步加载会阻拦页面的渲染,这时候如果有一个线程可以处理这个耗时js就好了。Web Worker可以创建一个新的进程,来和UI主线程并发运行。但是这个新进程不能操作Dom对象, 只能用来处理复杂的JS的操作。
<html>
<script src="耗时.js"></script>
<body>
<div>123</div>
</body>
</html>
使用方法:
// main.js
var worker = new Worker("task.js");
worker.postMessage(
{
id:1,
msg:‘Hello World‘
}
);
worker.onmessage=function(message){
var data = message.data;
console.log(JSON.stringify(data));
worker.terminate();
};
worker.onerror=function(error){
console.log(error.filename,error.lineno,error.message);
}
// task.js
onmessage = function(message){
var data=message.data;
data.msg = ‘Hi from task.js‘;
postMessage(data);
}
可以看到,两个js之间通过事件通讯。尽管Web Worker在大量数据的检索、数据运算上能提供一些帮助,但是使用面还是比较窄的。