浏览器浅谈

浏览器的架构

CPU和GPU的区别

把CPU的一个核心(core)比喻成一个办公室工人,他功能强大,上知天文下知地理,琴棋书画无所不能,它可以串行地一件接着一件处理交给它的任务。
CPU

每个GPU核心手里只有一个扳手,这就说明它的能力是非常有限的,可是它们人多啊!
GPU

进程和线程的区别

(进程是资源分配的最小单位,线程是CPU调度的最小单位)

做个简单的比喻:进程=火车,线程=车厢

  1. 线程在进程下行进(单纯的车厢无法运行)
  2. 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  3. 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  4. 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  5. 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  6. 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  7. 进程可以拓展到多机,线程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  8. 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
  9. 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

浏览器的架构

单进程和多进程浏览器的架构图
GPU

chrome采用的是多进程架构
GPU

各个进程如何分工合作呢?
GPU

  • Chrome多进程架构的好处
  1. 多进程可以使浏览器具有很好的容错性,如果所有的tab都跑在同一个进程的话,它们就会有连带关系,一个挂全部挂。
  2. 可以提供安全性和沙盒性(sanboxing)
  • Chrome多进程架构的坏处

进程的内存消耗。

由于每个进程都有各自独立的内存空间,所以它们不能像存在于同一个进程的线程那样共用内存空间;

为了节省内存,Chrome会限制被启动的进程数目,当进程数达到一定的界限后,Chrome会将访问同一个网站的tab都放在一个进程里面跑。所以当页面开多了,有些页面会出现 “Aw,Snap” 错误。

浏览器是如何运行的

当你在地址栏中输入地址回车之后,chrome是怎么运行的?

  1. 处理输入
    Browser Process进程的UI线程(UI thread)判断你输入的字符串是一些搜索的关键词(search query)还是一个URL地址呢?

  2. 开始导航
    当用户按下回车键的时候,UI线程会叫网络线程(network thread)初始化一个网络请求来获取站点的内容。这时候tab上会展示一个提示资源正在加载中的旋转圈圈,而且网络线程会进行一系列诸如 DNS寻址 以及为请求建立TLS连接的操作。
    如果网络线程收到服务器的HTTP 301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。

  3. 读取响应
    网络线程在收到HTTP响应的主体(payload)流(stream)时,在必要的情况下它会先检查一下流的前几个字节以确定响应主体的具体媒体类型(MIME Type)。响应主体的媒体类型一般可以通过HTTP头部的Content-Type来确定,不过Content-Type有时候会缺失或者是错误的,这种情况下浏览器就要进行MIME类型嗅探来确定响应类型了。MIME类型嗅探并不是一件容易的事情,你可以从Chrome的源代码的注释来了解不同浏览器是如何根据不同的Content-Type来判断出主体具体是属于哪个媒体类型的。
    如果响应的主体是一个HTML文件,浏览器会将获取的响应数据交给渲染进程(renderer process)来进行下一步的工作。如果拿到的响应数据是一个压缩文件(zip file)或者其他类型的文件,响应数据就会交给下载管理器(download manager)来处理。
    网络线程在把内容交给渲染进程之前还会对内容做 SafeBrowsing 检查。如果请求的域名或者响应的内容和某个已知的病毒网站相匹配,网络线程会给用户展示一个警告的页面。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

  4. 寻找一个渲染进程(renderer process)
    在网络线程做完所有的检查后并且能够确定浏览器应该导航到该请求的站点,它就会告诉UI线程所有的数据都已经被准备好了。UI线程在收到网络线程的确认后会为这个网站寻找一个渲染进程(renderer process)来渲染界面。

  5. 提交(commit)导航
    数据和渲染进程都已经准备好了,浏览器进程(browser process)会通过IPC告诉渲染进程去提交本次导航(commit navigation)。

渲染进程做了什么?

渲染进程里有什么进程?

  • 一个主线程(main thread)
  • 几个工作线程(worker threads)
  • 一个合成线程(compositor thread)
  • 一个光栅线程(raster thread)

1. 解析

  1. 构建dom
    渲染进程在导航结束的时候会收到来自浏览器进程提交导航(commit navigation)的消息,在这之后渲染进程就会开始接收HTML数据,同时主线程也会开始解析接收到的文本数据(text string)并把它转化为一个DOM(Document Object Model)对象。
  2. 子资源加载
    除了HTML文件,网站通常还会使用到一些诸如图片,CSS样式以及JavaScript脚本等子资源。这些文件会从缓存或者网络上获取。主线程会按照在构建DOM树时遇到各个资源的循序一个接着一个地发起网络请求,可是为了提升效率,浏览器会同时运行“预加载扫描”(preload scanner)程序。如果在HTML文档里面存在诸如或者这样的标签,预加载扫描程序会在HTML解析器生成的token里面找到对应要获取的资源,并把这些要获取的资源告诉浏览器进程里面的网络线程。
  3. JavaScript会阻塞HTML的解析过程
    当HTML解析器碰到script标签的时候,它会停止HTML文档的解析从而转向JavaScript代码的加载,解析以及执行。为什么要这样做呢?因为script标签中的JavaScript可能会使用诸如document.write()这样的代码改变文档流(document)的形状,从而使整个DOM树的结构发生根本性的改变。因为这个原因,HTML解析器不得不等JavaScript执行完成之后才能继续对HTML文档流的解析工作。

2.给浏览器一点如何加载资源的提示

web开发者可以通过很多方式告诉浏览器如何才能更加优雅地加载网页需要用到的资源。如果你的JavaScript不会使用到诸如document.write()的方式去改变文档流的内容的话,你可以为script标签添加一个async或者defer属性来使JavaScript脚本进行异步加载。当然如果能满足到你的需求,你也可以使用JavaScript Module。同时资源预加载可以用来告诉浏览器这个资源在当前的导航肯定会被用到,你想要尽快加载这个资源。

3.样式计算 - Style calculation

拥有了DOM树我们还不足以知道页面的外貌,因为我们通常会为页面的元素设置一些样式。主线程会解析页面的CSS从而确定每个DOM节点的计算样式(computed style)。计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,你可以打开devtools来查看每个DOM节点对应的计算样式。

即使你的页面没有设置任何自定义的样式,每个DOM节点还是会有一个计算样式属性,这是因为每个浏览器都有自己的默认样式表。

4.布局 - Layout

主线程会遍历刚刚构建的DOM树,根据DOM节点的计算样式计算出一个布局树(layout tree)。布局树上每个节点会有它在页面上的x,y坐标以及盒子大小(bounding box sizes)的具体信息。布局树长得和先前构建的DOM树差不多,不同的是这颗树只有那些可见的(visible)节点信息。

举个例子,如果一个节点被设置为了display:none,这个节点就是不可见的就不会出现在布局树上面(visibility:hidden的节点会出现在布局树上面)。同样的,如果一个伪元素(pseudo class)节点有诸如p::before{content:“Hi!”}这样的内容,它会出现在布局上,而不存在于DOM树上。

5.绘画 - Paint

在绘画这个步骤中,主线程会遍历之前的到的布局树(layout tree)来生成一系列的绘画记录(paint records)。绘画记录是对绘画过程的注释,例如“首先画背景,然后是文本,最后画矩形”。

6.高成本的渲染流水线(rendering pipeline)更新

渲染流水线有一个十分重要的点就是流水线的每一步都要使用到前一步的结果来生成新的数据,这就意味着如果某一步的内容发生了改变的话,这一步后面所有的步骤都要被重新执行以生成新的记录。举个例子,如果布局树有些东西被改变了,文档上那些被影响到的部分的绘画顺序是要重新生成的。

如果你的页面元素有动画效果(animating),浏览器就不得不在每个渲染帧的间隔中通过渲染流水线来更新页面的元素。我们大多数显示器的刷新频率是一秒钟60次(60fps),如果你在每个渲染帧的间隔都能通过流水线移动元素,人眼就会看到流畅的动画效果。可是如果流水线更新时间比较久,动画存在丢帧的状况的话,页面看起来就会很“卡顿”。

GPU

某些动画帧被JavaScript阻塞了

GPU

在动画帧上运行一小段JavaScript代码

7.合成

文档结构,元素的样式,元素的几何信息以及它们的绘画顺序,将以上这些信息转化为显示器的像素的过程叫做光栅化(rasterizing)。

可能一个最简单的做法就是只光栅化视口内(viewport)的网页内容。如果用户进行了页面滚动,就移动光栅帧(rastered frame)并且光栅化更多的内容以补上页面缺失的部分。Chrome的第一个版本其实就是这样做的。然而,对于现代的浏览器来说,它们往往采取一种更加复杂的叫做合成(compositing)的做法。