参考视频:浏览器是如何运作的

概论

浏览器的组成部分

浏览器分为以下几个部分:用户界面浏览器引擎渲染引擎

其中:

  • 用户界面用于展示除标签页窗口之外的其它用户界面内容
  • 渲染引擎负责渲染用户请求的页面内容
  • 浏览器引擎用于在用户界面和渲染引擎之间传递数据

渲染器下有很多小的功能模块,如负责网络请求的网络模块,用于解析和执行js的js解释器,还有数据存储持久层(帮助浏览器存储各种数据,如cookie)

渲染引擎是浏览器的核心,把它称作浏览器的内核

浏览器内核


Blink是基于Webkit改造的

进程与线程


  • 当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里

  • 当应用关闭时,该内存空间就会被回收,进程可以启动更多的进程来执行任务

  • 两个进程间相互独立,如果需要两个进程空间需要传递某些数据,则使用进程间通信管道IPC传递

  • 进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的任务

  • 同一进程下的线程是可以直接通信共享数据的

各进程的作用


  • 浏览器进程负责控制chrome浏览器除标签页外的用户界面,包括地址栏、书签、后退和前进按钮,以及负责与浏览器的其它进程协调工作

  • 网络进程负责发起接受网络请求

  • GPU进程负责整个浏览器界面的渲染

  • 插件进程 负责控制网站使用的所有插件(此处插件是flash等,不是chrome安装的拓展插件)

  • 渲染器进程用来控制显示tab标签内的所有内容

从输入网址到加载网页的过程


浏览器默认情况下,为每个标签页都创建一个进程

输入网址

在地址栏输入地址时,浏览器进程的UI线程会捕捉你的输入内容

如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着开始链接服务器获取数据

如果输入不是网址而是一串关键词,浏览器判断你要搜索,就会使用默认配置的搜索引擎来查询

当网络线程获取到数据后,会通过SafeBrowsing来检查站点是否是恶意站点,如果是,则提示警告页面(可强行继续访问)

SafeBrowsing是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全(比如通过查看该站点IP是否在谷歌黑名单之内)

当返回数据准备完毕并且安全校验通过时,网络线程会通知UI线程:自己已经准备好,然后UI线程会创建一个渲染器进程来渲染页面

渲染流程

获得DOM树

浏览器进程通过IPC管道将数据传递给渲染器进程,正式进入渲染流程

渲染器进程接收到的数据也就是html,渲染器进程的核心任务就是把html css js image等资源渲染成用户可以交互的web页面

渲染器进程的主线程将html进行解析,构造DOM数据结构(DOM是文档对象模型,是浏览器对页面在其内部的表示形式,是web开发程序员可以通过JS与之交互的数据结构和API)

html首先经过tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记进行DOM树构造,在DOM树构造过程中会创建document对象,然后以document为根节点的DOM树不断进行修改,向其中添加各种元素

html代码中往往会引入一些额外的资源,图片和CSS这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,不会影响DOM的生成

但当HTML解析过程中遇到script标签,将停止html解析流程,转而去加载解析并且执行JS,这是因为浏览器不知道JS执行是否会改变当前页面的HTML结构(JS代码可以用document.write 来修改html),这就是要把script标签放在合适的位置/或者使用async或defer属性来异步加载执行JS 的原因

HTML解析完成之后,获得DOM树,但我们还不知道其上每个结点长什么样子

获得Layout树

主线程需要解析CSS,并确定每个DOM结点的计算样式,即使没有提供CSS样式,浏览器也会有自己默认的样式表

我们接下来需要知道每个结点需要放在页面上的哪个位置,也就是结点的坐标以及该结点需要占用多大的区域,这个阶段被成为layout布局

主线程通过遍历dom和计算好的样式来生成Layout树,Layout树上的每个结点都记录了x,y坐标和边框尺寸

注意DOM树和Layout树并不是一一对应的,设置了display:none 的结点不会出现在Layout树上,而在before伪类中添加了content值的元素,content里的内容会出现在Layout树上,而不会出现在DOM树里,这是因为DOM树是通过html解析获得,并不关系样式,而Layout树是根据DOM和计算好的样式来生成的,Layout树是和最后展示在屏幕上的结点是对应的

栅格化与合成

我们还需要知道以什么样的顺序绘制这个结点

举例来说,z-index属性会影响结点绘制的层级关系,如果我们按照dom的层级结构来绘制页面,则会导致错误的渲染,所以为了保证在屏幕上展示正确的层级,主线程遍历Layout树创建一个绘制记录表,该表记录了绘制的顺序,这个阶段被成为绘制(paint)

知道绘制顺序后,现在该把这些信息转化成像素点,显示在屏幕上了,这种行为称为栅格化,chrome使用较为复杂的栅格化流程,叫做合成

合成是一种将页面上的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程中单独进行合成页面的技术,简单来说就是页面所有的元素按照某种规则进行分图层,并把图层都栅格化好了,然后只需要把可视区的内容组合成一帧展示给用户即可

主线程遍历Layout树生成Layer树,当Layer树生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化,由于一层可能像页面的整个长度一样大,合成器线程将它们切分成许多图块,然后将每个图块发送给栅格化线程,栅格化线程栅格化每个图块,并将它们存储在GPU内存中,当图块栅格化完成后,合成器线程将收集称为draw quads的图块信息,这些信息里记录了图块在内存中的位置,和在页面的哪个位置绘制图块的信息

根据这些信息合成器线程生成了一个合成器帧,然后合成器帧通过IPC传送给浏览器进程,接着浏览器进程将合成器帧传送到GPU,然后GPU渲染展示到屏幕上,当页面发生变化(比如滚动页面),都会生成一个新的合成器帧,新的帧再传给GPU,然后再次渲染到屏幕上

重排重绘和卡顿现象

  • 重排:当改变一个元素的尺寸位置属性时,会重新进行样式计算,布局、绘制以及后面的所有流程

  • 重绘:当改变元素的颜色属性时,不会重新触发布局,但还剩会触发样式计算和绘制

重排和重绘都会占用主线程,JS也运行在主线程

页面的卡顿现象:
当页面以每秒60帧的刷新率(每帧16ms)时才不会感到卡顿

重排重绘和JS运行会出现抢占执行时间的问题,如果写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作

如果在运行动画时还有大量的JS任务需要执行,由于布局、绘制和JS执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有剩余时间,JS就会拿到主线程的使用权,如果JS执行时间过长,就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿

卡顿的优化方案

  1. requestAnimationFrame()
    这个方法会在每一帧被调用,通过API的回调,我们可以把JS运行任务分成一些更小的任务块,分到每一帧,在每一帧时间冲完前暂停JS执行,归还主线程,这样的话在下一帧开始时,主线程就可以按时执行布局和绘制

  2. 由于栅格化的整个流程是不占用主线程的,只在合成器线程和栅格线程中运行,这就意味着它无需和JS抢夺主线程,通过transform属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程中JS执行的影响

用transform实现的动画由于不需要经过布局绘制,样式计算等操作,所以节省了很多运算时间