本文博客地址老崔的小破栈 (opens new window)

# 什么是浏览器内核?

一般我们常说的浏览器内核指的其实就是“渲染引擎”

# 浏览器的组成部分

浏览器的组成主要有三部分:用户界面,浏览器引擎以及渲染引擎

用户界面:这个其实就是我们平常使用浏览器看到的一部分

浏览器引擎:用于在用户界面和渲染引擎之间传递数据(一般来说这就是浏览器的数据持久层)

渲染引擎:负责渲染用户请求的页面内容

  • 渲染引擎包括多个小的功能模块如:

    网络模块:负责网络请求

    JS解析器:用于解析和执行JS

# 常见浏览器内核以及其发展史

这部分就比较生硬了,貌似所有介绍者都喜欢介绍这部分

image-20210706125850018

上图截取自油管视频主Luke Lee的视频

  • 1991年Berners Lee建立了第一代浏览器World Wide Web

Berners Lee是互联网发明者之一(妥妥的大佬)

World Wide Web 只支持显示图片和文本

  • 1993年Mosaic问世

Mosaic可以同时显示文本和图片

  • 1994年网景,OPERA发布

网景的开发人员有一部分曾参与Mosaic开发

只能显示HTML没有CSS和JS

相较于网景,opera使用较少

  • 1995年IE诞生

万恶之源?自此第一次浏览器大战正式打响

  • 1996年IE3.0发布且与windows融合

此时网景的市场份额仍有86%

  • 1998年网景公司成立Mozilla基金会

基金会用来开发火狐浏览器

  • 1999年IE占据99%市场

牛哇

  • 2003年Safari浏览器问世

觉得眼熟?这就是苹果专用浏览器

  • 2004年Firefox(火狐)问世

第二次浏览器大战序幕拉开

  • 2005年苹果公司将webkit内核开源

为浏览器发展贡献巨大,开源yyds

  • 2008年googlewebkit为核心开发chromechromium

最强浏览器诞生?

  • 2015年微软推出以webkit为内核的浏览器edge

来晚咯

# 各浏览器对应内核

IE: Trident

FireFox: Gecko

Safari: Webkit

Chrome,Opera,Edge: Blink(基于Webkit开发的)

# 以Chrome为例介绍浏览器运行

本质:浏览器本质上是运行在操作系统上的一个应用程序

应用程序的特征:每个应用程序至少启动一个进程来执行功能

进程执行过程中由于有许多任务所以会创建线程来帮助执行

进程:是操作系统进行资源分配和调度的基本单元,可以申请和拥有计算机资源,进程是程序的基本执行实体

线程:线程是操作系统能够进行运算调度的最小单位,一个进程中可以并发多个线程,每条线程并行执行不同的任务

通俗点讲:应用程序启动后会创建多个进程。各个进程之间的内存空间不同,因此相互独立,进程之间通信需要通过进程间通信管道IPC来实现,每个进程可以将任务分成更多细小的人物,并通过创建多个线程并行执行不同的任务,同一进程下的线程是可以直接通信共享数据的

# 浏览器进程介绍

早期浏览器使用单进程

  • 不稳定:其中一个线程的卡死会导致整个进程卡死
  • 不安全:由于同一进程下的线程可以共享数据,js线程可以直接访问浏览器进程内的所有数据
  • 不流畅:一个进程需要负责太多事情会导致运行效率问题

现代浏览器使用多进程

image-20210706133613130

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

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

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

插件进程:负责控制网站使用的所有插件(如flash等)

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

浏览器在默认情况下会为每个标签页创建一个渲染器进程,这个操作与你启动Chrome时选择的进程模型有关

# Chrome四种进程模型:

  • process-per-site-instance(default):为用户访问的每个实例创建一个渲染器进程,这样可以确保来自不同站点的页面是独立呈现的,并且对同一站点的单独访问也是彼此隔离的,简单来说就是访问不同站点或者访问同一站点的不同页面都会产生新的进程
  • process-per-site:同一站点使用同一进程
  • process-per-tab:一个tab里的所有站点使用同一个进程
  • single-process:让浏览器引擎和渲染引擎共用一个进程

# 当你在浏览器地址栏输入网址

你知道当你在浏览器地址栏输入网址时,你的浏览器会做什么嘛:

# 浏览器进程

  • 首先,浏览器进程的UI会捕捉输入内容:

    • 如果访问的是网址,则UI线程会启动一个网络线程来请求DNS域名解析,接着开始连接服务器获取数据
    • 如果输入的是一串关键词,浏览器会使用默认配置的搜索引擎来查询
  • 网络线程获取到数据之后:

    当网络线程获取的数据后会通过SafeBrowsing来检查站点是否是恶意站点,如果是则会提示警告,当然你也可以强制访问

    image-20210706135600907

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

    当返回数据准备完毕并通过安全检验时,网络线程会通知UI线程,UI线程会创建一个渲染器进程来渲染页面,浏览器进程通过ICP将页面数据传递给渲染器进程

# 渲染器进程

  • 渲染器进程接受到数据(HTML等),
  • 核心任务是:html,css,js,image等资源渲染成用户可以交互的web页面
  • 渲染器进程的主线程将html进行解析构造DOM数据结构

DOM(文档对象数据模型)是浏览器对页面在其内部的表示形式。是程序员通过js可与之交互的数据结构和API

# HTML渲染主要步骤:

Html的解析主要包括两个部分

  1. Tokeniser阶段

    通过词法分析将HTML解析为多个标记 这是一个基于事件的HTML文本解析过程,最后会生成一个Token序列输出,当然此过程中来自网络的HTML不一定是整个文档,网络端接收到一部分HTML字节流时就会通知解析机解析该部分的内容。

  2. TreeConstruction阶段

    根据识别后的标记进行DOM树构造 Tokeniser阶段的Token序列产生后,就会把序列一个一个地输入到TreeConstruction中,最后输出DOMTree。

image-20210706141138135

HTML解析过程中的几个重要点:

  1. CSS,image等静态资源需要从网络/本地存储中获取,这些资源加载不会阻塞html的解析
  2. JS执行会阻塞html解析,其原因是浏览器无法确认js是否会改变dom结构,因此遇到script标签会直接执行,所以我们需要使用async或者defer属性异步加载js
  • 主线程解析完HTML后开始解析CSS(默认CSS或自定义CSS)

    image-20210706141659015

  • 主线程解析完CSS后要确定元素在页面的位置。这个过程被称作layout布局

    image-20210706141905267

  • 主线程通过遍历dom和计算好的样式来生成layout tree

![image-20210706142031568](https://chqosssave.oss-cn-beijing.aliyuncs.com/img/layout tree.png)

dom tree和layout tree不是一一对应的,设置了display:none的dom节点不会出现在layout tree上

before伪类中添加了content值的元素,content里的内容会出现在layout tree上,不会出现在dom树里,这是因为dom tree是通过HTML解析获得,并不关心样式,而layout tree则是根据DOM和计算好的样式来生成

  • 接下来主线程需要确定以什么样的顺序绘制节点

  • 主线程遍历layout tree创建绘制记录表(Paint Record),该表记录了绘制的顺序(这个阶段被称为绘制)

    image-20210706142812893

  • 现在一切准备就绪,到了将这些信息转化为像素点显示到屏幕上的时候了,这个行为叫做栅格化(Rastering)

    • 早期Chrome使用极其简单的方式进行栅格化,即只栅格化用户可见区域的内容,造成的缺点就是展示延迟(当你向下滚动时要等待页面加载)
    • 而现在Chrome采用更为复杂的栅格化方式:合成(Compositing):将页面的多个部分分成多个图层,分别对其进行栅格化,并在合成器线程(Compositor Thread)中单独进行合成页面的技术,简单来说就是页面所有的元素按照某种规则进行分图层并把图层都栅格化好了,然后只需要把可视区域的内容组合成一帧展示给用户即可

    image-20210706143311472

  • 主线程遍历layout tree生成layer(图层) tree,当layer tree生成完毕和绘制顺序确定后

    ![image-20210706143811820](https://chqosssave.oss-cn-beijing.aliyuncs.com/img/layer tree.png)

  • 主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化,合成器会将一个大页面分割成许多图块(tiles),然后将每个图块发送给栅格化线程

  • 栅格线程栅格每个图块,并将它们存储在GPU内存中

  • 在栅格化线程结束后,合成器线程会收集称为“draw quads”的图块信息,这些信息记录了图块在内存中的位置和在页面哪个位置绘制图块的信息

  • 根据这些信息合成器线程生成了一个合成器帧(Compositor Frame)

    image-20210706144501182

  • 然后合成器帧通过IPC传送给浏览器进程,浏览器进程将合成器帧传送到GPU,然后GPU渲染展示到屏幕上(页面渲染完成)

  • 当页面滚动发生变化,则会生成新的合成器帧让后继续上述步骤

  • 总流程如下:

    image-20210706144908915

  • 当排版发生改变,我们会重复上图过程,这个过程我们称为重排

  • 当我们改变某个元素的颜色属性时,不会触发重排,但是会触发样式的计算和绘制,这个过程称为重绘

    重排和重绘都会占用主线程,而js也会占用主线程,当页面以60帧每秒(也就是每帧16ms)的刷新率时,用户才不会觉得卡顿,当页面在一帧的时间内布局和绘制结束后还有剩余时间,JS会获得主线程的使用权,如果JS执行时间过长就会导致下一帧开始时JS没有及时归还主线程,导致下一帧动画没有进行渲染,就会出现页面动画卡顿

  • 优化手段:

    • 通过requestAnimationFrame()来处理:

      这个方法会在每一帧被调用,我们可以将js任务分为更小的任务块,在每一帧时间用完前暂停js执行归还主线程

    React 最新的渲染引擎React fiber就用到这个API做了大量优化

    • CSS中有个动画属性叫做transform通过该属性实现动画不会经过布局和绘制,而是直接运行在合成器线程和栅格线程,不会占用主线程,更不会收到主线程中JS的影响

    另外,transform动画由于不需要经过布局绘制,样式计算等操作,可以节省大量运算时间(方便实现动画)

    transform动画取代原有动画实现

    image-20210706150318559