banner
NEWS LETTER

浏览器

Scroll down

浏览器的高层结构

img

UI

地址栏、前进后退、书签菜单等。除了网页窗口的其他部分

浏览器引擎

UI和渲染引擎之间的编组操作

渲染引擎

渲染引擎的职责是渲染,可渲染HTMLXML和图片。可共通过插件或者扩展程序渲染其他类型的数据比如PDF。

不同浏览器使用不同的渲染引擎:IE使用TridentFirefox 使用GeckoChromeOpera使用BlinkBlinkwebkit的一个分支),Safari使用Webkit

渲染引擎的基本流程

渲染引擎的基本流程

  1. 首先从网络层获取文档的内容(HTML为例)
  2. 开始解析HTML内容,将元素转化为DOM节点,生成content tree。解析外部CSS文件和内联样式,创建成render tree
  3. render tree构建完毕,进入layout布局流程,为每个节点提供在屏幕上出现的具体位置坐标。然后借助UI后端,遍历render tree来绘制每个节点

【注】:内容的呈现是一个渐进的过程,浏览器不会等到所有HTML解析完成才开始构建树和布局,而是对部分内容进行解析和展示,并持续获取和处理来自网络层的内容。

渲染引擎主流程细节

WebKit main flow.

图2 WebKit渲染引擎主流程
Mozilla's Gecko rendering engine main flow.
图3 Mozilla’s Gecko 渲染引擎主流程
webkitgecko的渲染主流程有些许差异,总体差别不大。

解析流程(parser)

解析是指将文档(如html,xml)转换为可供代码使用的结构,解析的结果通常是表示文档结构的节点树(称为解析树或语法树)
解析的两个子过程:词法分析语法分析

词法分析和语法分析掌握不深,具体要通过《编译原理》课程学习

html解析器

html解析的结果是DOM(Document Object Model),是一个由dom节点和属性节点组成的树,树的根节点是document对象。
传统的解析器不适用于html解析,最主要的原因是:HTML 允许错误的存在,比如会自动添加缺少的结束标记等
HTML5规范 标记化和树构建的完整算法

CSS解析器

CSS是一种上下文无关的语法,通过CSS规范中定义的CSS词法和CSS语法,可以用各种解析器进行解析。
webkit 使用 FlexBison 解析器生成器

处理 scriptstyle 的顺序

  • 一般情况下,遇到<script>标签时会停止解析文档,并执行脚本。如果是从外部网络请求获取的脚本,会先下载再执行脚本内容,同样会停止HTML文档的解析。
  • 通过为 <script> 标签设置defer属性可以不阻塞文档解析,而是在后台异步下载脚本,在下载完脚本文件后,浏览器不会立即去执行它,而是会等到整个dom被解析完成后并且按照所有设置了defer属性的 scriptHTML中出现的顺序逐个执行。
  • script标签设置 async 属性,表示不阻塞HTML文档的解析过程,后台异步下载脚本文件,在下载完文件后立即执行它,这一点与 defer 属性不同。因此浏览器并不保证 async 异步脚本的执行顺序,只要下载完成就会立即执行。

【注】defer属性和async属性仅对设置了src属性的script(即外部文件)有效

1
2
<script defer src="xxx/a.js"></script>
<script async src="xxx/b.js"></script>

【more】对于手动创建script标签并插入dom这一操作,插入dom以及后续script的解析是同步的,但在插入dom后请求远程script文件这一步骤是异步的,浏览器在请求过程还会继续解析页面其他部分

而对于CSS样式表:

  • 在HTML解析的过程中,遇到样式表<link href="style.css" rel="stylesheet">,浏览器会异步地去下载css文件。
  • 但是在构建render tree 的过程,由于 CSSDOM 会影响 render tree的结果,所以在一步css样式的解析会阻塞
  • 由于script能够修改dom和CSSDOM,在执行script之前需确保所有的css样式都已下载解析完成

如果存在仍在加载和解析的样式表,Firefox 会阻止所有脚本。 只有当脚本尝试访问可能会受未加载的样式表影响的特定样式属性时,WebKit 才会阻止脚本。

因此最佳实践是:
<script>放在页面底部,确保html和css都加载并解析完毕;
将具有defer属性的<script>放在<head>内,确保scriptdomcss都解析完成后再执行;
确保<link><script>之前加载

推测解析器

在执行脚本时,另一个线程会解析文档的其余部分,找出并加载需要从网络加载的其他资源.通过这种方式,可以在并行连接上加载资源,从而提高整体速度。注意:推测性解析器仅解析对外部资源(如外部脚本、样式表和图片)的引用:它不会修改 DOM 树,而 DOM 树留给主解析器的引用。

构建渲染树

在浏览器解析HTML得到DOM树后,为了将页面呈现到屏幕上,浏览器会构建另一棵树,称为 渲染树,它是文档的可视化表示,旨在确保按照正确的顺序绘制内容。
WebKit的RenderObject类具有以下定义:

1
2
3
4
5
6
7
8
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node;  //the DOM node
RenderStyle* style;  // the computed style 
RenderLayer* containgLayer; //the containing z-index layer
}

RenderObject 类是渲染对象(render object)的基类,每个渲染对象表示一个矩形区域,对应到CSS盒模型,包含宽度,高度,位置等几何信息。

渲染树和DOM树的关系

渲染对象对应到DOM元素,但不是一一对应

  • 不可视DOM元素不会插入到渲染树中,比如head元素;display:none的元素也不会插入到渲染树中;但visibility: hidden的元素会插入到渲染树中,只是不可见。
  • 存在一个DOM元素对应多个可视对象的情况,这通常出现在那些无法用单个矩形来描述的复杂结构,比如<select>元素,就有三个元素,一个显示区域,一个下拉框,一个按钮。
  • 当文本一行容纳不下而需要占用多行时,新行会是一个额外的渲染对象。这也是一个DOM元素对应对个渲染对象的情况
  • 某些情况,渲染对象和它对应的DOM元素,处于树的不同位置。浮动元素和绝对定位的元素位于正常的文档流之外,根据float、position、top、bottom等值来计算和呈现它们在页面的实际位置,这些元素在原本正常文档流的位置会有一个占位元素。

树的构建流程

样式计算

CSS的来源包括:各种来源的样式表、内联样式、HTML虚拟属性(如bgcolor属性)。其中样式表的来源有浏览器默认样式表、页面引用的样式表以及用户样式表
样式计算存在的问题:

  • 样式表是一个大且复杂的结构,包含许多样式属性,可能造成内存问题
  • 为元素匹配样式同样可能存在性能问题,列表的遍历
  • 应用这些规则涉及到相当复杂的级联规则
    共享样式数据
    FireFox规则树

布局

渲染对象被创建并插入到渲染树时,并不具有位置和大小,计算这些值的过程称为布局或重排
HTML使用基于流的布局模型,大部分情况下只需一次就能计算出元素的几何图形。位于流下游的元素通常不会影响流上游的元素,因此布局过程可以按照从左往右、从上到下的过程,table元素是一个例外

布局是一个递归的过程,开始于根渲染对象,对应HTML文档的<html>元素。根渲染对象的坐标是$(0, 0)$,其尺寸就是viewport(浏览器可见窗口)。所有渲染对象都有一个 layoutreflow 方法,每个渲染对象都会调用其子元素的layout方法

脏位系统(dirty bit system)

为避免每次小改动都触发全局layout,浏览器使用脏位系统,被添加或被修改的渲染对象将为自己及其子元素添加标记:dirty,这个标记有两种:渲染对象本身is dirty、至少有一个子元素is dirty

全局布局和增量布局
在整个render tree触发布局是 全局布局,有两种情况会触发 全局布局

  1. 会影响所有渲染对象的全局样式属性的修改,比如font-size
  2. 屏幕大小变化

网络

todo

JavaScript解释器

解释和执行javascript,Chrome使用V8引擎

V8引擎

V8是一个开源的JavaScript引擎,Chrome浏览器和Nodejs都使用V8来解释执行JS代码。V8执行JS代码

编程语言按照从源代码机器语言的过程可分为:

  1. 解释型语言:由解释器逐行翻译为机器语言并马上执行,无需等到所有源代码都翻译完成再执行,代表语言有:PythonJavaScriptRuby
  2. 编译型语言:由编译器将所有源代码一次性翻译成机器代码,得到一个可执行文件(并非所有都会生成可执行文件,像有些引入了JIT的语言编译的结果是中间码),可脱离源代码独立运行,代表语言:CC++GoJava

还有一种介于解释型和编译型之间的即时编译(JIT,Just In Time),将源代码先翻译为中间码,在运行时再将中间码编译为机器码。这样做的好处是可以做到跨平台,代表语言:C#

分类并不绝对,一些语言也能使用JIT编译器来提高性能

【注】:有些语言如CC++会在编译过程生成汇编器中间语言作为中间代码,但是这需要与JIT的中间码区分开来,JIT生成的中间码是一种内部使用的中间表示,主要用于提供程序运行效率,通常给虚拟机或解释器内部使用。
例外:(Java的字节码既可以看成中间代码,也可以理解为是一种内部使用的中间码(能提高运行效率),偏向认为它是一种中间代码,具有平台无关性)

JavaScript是一种解释型语言,V8引入JIT来提高性能

V8中的对象存取优化

==indexed properties vs named properties ==
indexed properties通常见于array类型,named properties常见于object类型
一般来说,indexed properties存储在单独的elements隐藏属性里。named properties存储在当读的properties隐藏属性里,elements隐藏属性和properties隐藏属性的数据结构是array或dictionary中的一种

HiddenClass(隐藏类)是什么?有什么用
HiddenClass用来保存对象结构相关的元信息,包括属性的数量指向对象原型的指针,从概念上类似于面向对象编程的类的概念。在V8中,js object的第一个区域指向HiddenClass

HiddenClass的内容
bit field 3比较重要,保存着对象属性的数量以及执行 descriptor array 的指针,

HC是即时创建和动态修改的

拥有同样名称且同样顺序属性的对象共用一个HiddenClass,每次添加新属性会触发将HiddenClass修改为一个新的HiddenClass,在V8后台,通过一个过渡树(transition tree)将隐藏类链接在一起。但是添加==数组索引属性==不会创建新的隐藏类。
Pasted image 20240417142556

V8中对象属性是如何存储的?
三种方式:
in-object:直接存储在对象本身,这种方式访问最快。in-object类属性的数量由对象的初始大小决定,超出的属性会存储在properties隐藏属性里,properties隐藏属性指向一片新的可动态增加的空间(可以是线性结构也可以是字典类型)
fast-properties/linear:线性结构,访问很快,但属性很多的情况添加和删除操作比较耗时。
slow-properties/dictionary:字典类型的Key-Value结构,

fast properties VS. slow properties
一般来说,数组元素和索引型属性(数字)存储为快属性
线性存储方式,可通过索引直接访问,称为快属性
字典存储方式,以key-vaule的方式访问,属于慢属性

属性存放在哪里不是固定的,V8会根据属性访问频率等调整存储位置,这是一种叫做的inline caches的优化方式

ui后端

TODO

数据存储

持久性层,在本地保存各种数据,包括cookie,localstorage、indexedDB,webSQL,filesystem等

webStorage

cookie/localStorage/sessionStorage都是webStorage的一部分

存储机制 有效期 同步or异步 存储大小
localStorage 持久化存储在浏览器中 同步,会阻塞主线程 5MB,只能字符串
sessionStorage 仅限于当前标签页的生命周期,\n恢复或刷新页面不会清除,不同的tab页即使是同样的url也会各自创建独立的sessionStorage,这十分适合于那些需要临时数据隔离避免持久化污染的场景,比如表单草稿,保存单页面路由组件状态 同步,会阻塞主线程 5MB,只能字符串
Cookie 限字符串

Cache API

OPFS 来源私有文件系统

IndexedDB

Service Workers

Service Workers是一种浏览器技术,他们作为web app和浏览器之间的代理,能够拦截和处理网络请求,可以实现离线支持、消息推送、资源缓存、后台同步
Service Workers只能在https环境下工作

service workers需要注册

1
2
3
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./serviceWork脚本.js)
}

浏览器是多进程的

进程:CPU分配资源的最小单位
线程:CPU调度的最小单位,一个进程可以有多个线程

  1. ==Browser进程==:浏览器主进程,只有一个,作用是:

    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  2. ==GPU进程==:最多一个,用于3D绘制

  3. ==浏览器渲染进程==:渲染进程是多线程的,默认情况是每个tab标签页一个进程,当进程达到一定数量时,同协议和域名的标签页会被放到同一个渲染进程

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

  1. GUI渲染线程
    负责渲染界面

  2. JS引擎线程
    负责处理js脚本程序

  3. 事件触发线程
    属于浏览器,用于控制事件循环

  4. 定时触发器线程
    setTimeoutsetInterval所在线程

  5. 异步http请求线程

浏览器 & 网络

web api

web api属于是BOM的扩展部分,是由浏览器提供的功能

clipboard

w3c规定clipboard必须支持三种类型的数据

  • text/plain
  • text/html
  • image/png
    原本有更多的数据类型是支持的,写入有8种,读取有16种,后来出于安全考虑,只保留了上面三种。
    ==clipboardEvent==
    1
    2
    3
    4
    5
    6
    dom.addEventListener('copy', (e) => {
    e.preventDefault();

    e.clipboardData.setData('text/plain', 'hello');
    e.clipboardData.setData('text/html', '<p>hello</p>');
    })
    cutcopypaste事件监听器的成功执行必须是由用户来触发,即command+c ``command+v等行为,通过js脚本来dispatch是不允许修改和访问的,通过e.isTrusted可以判断是否可信
    document.execCommand('copy')可以做到以可信的方式通过程序调度复制事件

Observer api

BF Cache 浏览器往返缓存

后退/前进缓存是现代浏览器提供的一项性能增强特性,运行用户在之前访问过的页面之间进行即时的前进后退。它通过在用户导航离开页面时存储页面的完整快照来实现这一点;如果用户决定返回页面,浏览器可以快速恢复快照,而不需要重复加载页面所需的网络请求。

bfcache缓存在内存中,与http缓存不同,整个页面的完整快照都存于内存中,而http只包含页面发出的请求响应。
存在于bfcache中的页面,其中的待处理的计时器和未解析promise全部都会被暂停,在页面从bfcache中恢复后继续处理任务。
用于观察bfcache的主要事件是页面转换事件pageshowpagehide。网页首次加载以及从bfcache恢复页面时,pageshow会在load事件后立即触发。可以通过pageshow事件的persisted属性判断是否是从bfcache恢复的,是为true,否则false

CORS跨域

相关的http请求头

access-control-request-headers 请求头
access-control-allow-headers 响应头

参考资料

  1. How browsers work
其他文章