Skip to content

Latest commit

 

History

History
1689 lines (1021 loc) · 74.6 KB

浏览器工作原理与实践.md

File metadata and controls

1689 lines (1021 loc) · 74.6 KB

浏览器工作原理与实践

浏览器发展历程

进化路线:

  • 应用程序Web化
  • Web应用移动化
  • Web操作系统化

1、宏观视角下的浏览器

1.1、Chrome架构

通过浏览器的多进程架构的学习,可以把这些分散的知识点(浏览器中的网络流程、页面渲染过程,JavaScript 执行流程,以及 Web 安全理论等)串起来,组成一张网,从而站在更高的维度去理解 Web 应用。

【进程和线程】

进程是一个程序的运行实例。

启动一个程序时,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,这样的一个运行环境就叫做进程

线程由进程来启动和管理,一个进程可以有多个线程。

Untitled

进程和线程之间的关系:

  1. 进程中的任一线程出错,都会导致整个进程崩溃
  2. 线程之间共享进程中的数据
  3. 当某进程关闭后,操作系统会回收该进程所占用的内存资源
  4. 进程之间的数据相互隔离,可以使用IPC进行通信

进程结束时,进程所占资源会被操作系统回收,但是当一个线程结束后,却有可能发生内存泄漏的情况。

【早期单进程浏览器】

单进程浏览器是指浏览器所有的功能模块都运行在同一个进程中。

Untitled

单进程浏览器缺点:

  • 不稳定

一个插件的崩溃将会导致整个浏览器都崩溃,渲染引擎的崩溃也会导致整个浏览器都崩溃。

  • 不流畅

所有模块都运行在同一个进程中,意味着同一时刻只能有一个模块可以运行,会阻塞其他脚本或插件等模块的执行,导致浏览器卡顿。

页面的内存泄漏也是单进程变慢的重要原因。运行一个复杂点的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是使用时间越长,内存占用越高,浏览器会变得越慢。

  • 不安全

插件可以获取到操作系统的任意资源,页面脚本也能通过浏览器漏洞获取系统权限。匹夫无罪,怀壁自罪。

【现代多进程浏览器】

多进程浏览器优点:

  • 进程相互隔离,所以解决了不稳定、不流畅、内存泄漏的问题
  • 可以使用安全沙箱机制

安全沙箱相当于给一个进程上了一把锁,进程在箱子里面,可以正常运行,但是不能跑出箱子,不能获取最高权限,因此也就从根本上杜绝了该进程的安全风险。

Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。

早期多进程浏览器架构:

  • 浏览器主进程
  • 渲染进程
  • 插件进程

Untitled

现代多进程浏览器架构:

  • 浏览器进程

主要负责界面显示、用户交互、子进程管理,同时提供存储等功能

  • GPU进程

Chrome 刚开始发布的时候是没有 GPU 进程的。

而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程

主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 渲染进程

核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。

排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。

出于安全考虑,渲染进程都是运行在沙箱模式下。

  • 插件进程

主要负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

Untitled

打开一个网页,最少需要四个进程。即一个浏览器主进程、一个网络进程、一个GPU进程和一个渲染进程,插件进程是可选的。

多进程浏览器缺点:

  • 更高的资源占用
  • 更复杂的体系结构

【未来面向服务的架构】

为了解决多进程浏览器架构的缺点,在 2016 年,Chrome 官方团队使用“面向服务的架构”(Services Oriented Architecture,简称 SOA)的思想设计了新的 Chrome 架构。

也就是说 Chrome 整体架构会朝向现代操作系统所采用的“面向服务的架构” 方向发展,原来的各种模块会被重构成独立的服务(Service)。

每个服务(Service)都可以在独立的进程中运行,访问服务(Service)必须使用定义好的接口,通过 IPC 来通信,从而构建一个更内聚松耦合易于维护扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标。

面向服务的架构:

Untitled

灵活的弹性架构:

Untitled

Chrome架构总结

进程和线程之间的关系:

  1. 进程中的任一线程出错,都会导致整个进程崩溃
  2. 线程之间共享进程中的数据
  3. 当某进程关闭后,操作系统会回收该进程所占用的内存资源
  4. 进程之间的数据相互隔离,可以使用IPC进行通信

单进程浏览器时代

缺点:

  • 不稳定
  • 不流畅
  • 不安全

多进程浏览器时代

优点:

  • 进程相互隔离,所以解决了不稳定、不流畅、内存泄漏的问题
  • 可以使用安全沙箱机制

【早期多进程浏览器架构】

  • 浏览器主进程
  • 渲染进程
  • 插件进程

【现代多进程浏览器架构】

  • 浏览器主进程
  • 网络进程
  • GPU进程
  • 渲染进程
  • 插件进程

缺点:

  • 更高资源占用
  • 更复杂的体系结构

【未来面向服务的架构】

优点:

  • 更内聚
  • 松耦合
  • 易于维护
  • 可扩展

1.2、TCP协议

名称 说明 备注
IP 网络协议(Internet Protocol) IP 通过 IP 地址信息把数据包发送给指定的电脑
UDP 用户数据包协议(User Datagram Protocol) UDP 通过端口号把数据包分发给正确的应用程序;UDP 不能保证数据可靠性,但是传输速度却非常快
TCP 传输控制协议(Transmission Control Protocol) 一种面向连接的、可靠的、基于字节流的传输层通信协议。建立连接(三次握手);传输数据;断开连接(四次挥手)

一个TCP连接的生命周期:

Untitled

网络协议中代表数据包类型的标志:

  • 【SYN】请求建立连接类型的数据包标志
  • 【SYN、ACK】既请求建立连接,又响应确认建立连接
  • 【ACK】确认建立连接类型的数据包标志
  • 【FIN】请求断开连接的数据包标志

TCP/IP协议指的不仅仅是TCP和IP这两个协议,而是由FTP、SMTP、TCP、UDP、IP等各种协议组成的协议簇,因为TCP/IP最具代表性,所以统称为TCP/IP协议。

三次握手:

两个机器之间建立TCP/IP连接,需要进行三次握手,既进行三次数据包的交互。进行SYN建立连接请求发送,以及ACK确认建立连接的数据包响应。

流程:

  • Client:【SYN】
  • Server:【SYN、ACK】
  • Client:【SYN】

四次挥手:

挥手1:服务端发出请求数据包【FIN】,请求断开连接

挥手2:客户端回复服务端确认数据包【ACK】,确认要断开连接

挥手3:客户端发出【FIN】,终止TCP连接的请求,【ACK表示对上一次请求的确认】

挥手4:服务端发出【ACK】,确认收到的【FIN】终止TCP连接报文

流程:

  • Server:【FIN、ACK】
  • Client:【ACK】
  • Client:【FIN、ACK】
  • Server:【ACK】

常见端口号:

应用程序 FTP TFTP TELNET SMTP DNS HTTP SSH MYSQL
熟知端口 21,20 69 23 25 53 80 22 3306
传输层协议 TCP UDP TCP TCP UDP TCP TCP TCP

socket套接字:

socket套接字就是IP+端口(IP+Port)的具象化,比如:10.0.0.7:80,其存在的意义就是,让两端进行数据交互,数据传输。

部署nginx服务,运行在 10.0.0.7:80 端口,这就是一个socket,然后通过本地浏览器随机指定的端口,发出请求,去访问这个socket,这就构成了一个socket通信。

socket通信:

任何两个机器的连接,指的是TCP/IP协议的连接,本质上是两个socket的通信

OSI七层协议:

  • 物理层
  • 链路层
  • 网络层
  • 传输层
  • 会话层
  • 表示层
  • 应用层

Untitled

TCP协议总结

  • 互联网中的数据是通过数据包来传输的,数据包在传输过程中容易丢失或出错。
  • IP 负责把数据包送达目的主机。
  • UDP 负责把数据包送达具体应用。而 TCP 保证了数据完整地传输,它的连接可分为三个阶段:建立连接、传输数据和断开连接。

1.3、HTTP请求流程

概念:HTTP 是一种允许浏览器向服务器获取资源的协议,是 Web 的基础。

HTTP协议和TCP协议,都是TCP/IP协议簇的子集。

HTTP协议属于应用层,TCP协议属于传输层,HTTP协议位于TCP协议的上层。

webSocket也属于应用层协议,可以把websocket看成是http的改造版本,在其基础上新增了服务器向客户端主动发送消息的能力。

Web服务器工作原理:

  • 客户端通过TCP/IP协议建立到服务器的TCP连接
  • 客户端项服务端发送HTTP协议请求包,请求服务器里的资源
  • 服务器向客户端发送HTTP应打包,若请求的资源包含有动态语言的内容,则服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
  • 客户端与服务器端断开连接。由客户端解释HTML文档,在屏幕上渲染图形结果

客户端请求到达服务端流程:

  • 当客户端拿到服务端域名对应的IP后,浏览器会以一个随机端口(1024<随机端口<65535)向服务器的Web程序(nginx、apache)的80端口发起TCP连接请求
  • 该请求经过复杂的网络环境后到达服务端,进入到服务器的对应的网卡,再进入到Linux内核的TCP/IP协议栈,一层一层的解开数据包,甚至经过防火墙,最终到达nginx程序,确认TCP/IP连接
  • 确认TCP连接之后,客户端继续发起HTTP请求,如常见的get、post等请求

Untitled

浏览器端发起HTTP请求流程:

  1. 构建请求
  2. 查找缓存
  3. 准备IP地址和端口
  4. 等待TCP队列
  5. 建立TCP连接
  6. 发送HTTP请求

服务端处理HTTP请求流程:

  1. 返回请求
  2. 断开连接
  3. 重定向

负责把域名和 IP 地址做一一映射关系的系统就叫做“域名系统”,简称DNS(Domain Name System)

浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。

总结:

1.4、导航流程

Untitled

从输入 URL 到页面展示:

  1. 用户输入
  2. URL请求过程
    1. 重定向
    2. 相应数据类型处理
    3. 准备渲染进程
    4. 提交文档
    5. 渲染阶段

URL 请求

  1. 浏览器进程通过 IPC 把 URL 请求发送至网络进程
  2. 查找资源缓存(有效期内)
  3. DNS 解析(查询 DNS 缓存)
  4. 进入 TCP 队列(单个域名 TCP 连接数量限制)
  5. 创建 TCP 连接(三次握手)
  6. HTTPS 建立 TLS 连接(client hello, server hello, pre-master key 生成『对话密钥』)
  7. 发送 HTTP 请求(请求行[方法、URL、协议]、请求头 Cookie 等、请求体 POST)
  8. 接受请求(响应行[协议、状态码、状态消息]、响应头、响应体等)
    • 状态码 301 / 302,根据响应头中的 Location 重定向
    • 状态码 200,根据响应头中的 Content-Type 决定如何响应(下载文件、加载资源、渲染 HTML)

Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance。

总结:

  • 服务器可以根据响应头来控制浏览器的行为,如跳转、网络数据类型判断。
  • Chrome 默认采用每个标签对应一个渲染进程,但是如果两个页面属于同一站点,那这两个标签会使用同一个渲染进程。
  • 浏览器的导航过程涵盖了从用户发起请求到提交文档给渲染进程的中间所有阶段。

1.5、渲染流程

浏览器渲染页面的过程

浏览器渲染页面的过程:

  1. 输入网址

    用户在地址栏输入查询关键字,地址栏判断其是搜索内容还是请求的URL。

    若是搜索内容,地址栏会使用搜索引擎,合成新的带关键字的URL;

    若是请求URL,则地址栏会根据规则,将此内容加上协议,合成为一个完整的URL。

    用户键入关键字,并点击回车前,浏览器执行了一次beforeunload事件。

  2. 解析URL

    浏览器解析出URL信息,构造一个http请求。

    URL主要由协议、主机、端口、路径、查询参数和锚点六部分组成。

  3. 检查浏览器缓存

    网络进会检查浏览器本地是否缓存了对应的数据资源。

    若有,则返回资源给浏览器进程;若无,则进入网络请求进程。

  4. DNS解析

  5. TCP/IP连接

  6. http请求

  7. 服务器请求并返回报文

    服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息)并发给网络进程。

    等网络进程接收了响应行和响应头之后,就开始解析内容。

  8. 渲染页面

    字节 → 字符 → 令牌 → 节点 → 对象模型

  9. 断开连接

渲染模块在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做渲染流水线,其大致流程如下图所示:

Untitled

按照渲染的时间顺序,流水线可分为如下几个子阶段:

  • 构建DOM树
  • 样式计算
  • 布局阶段
  • 分层
  • 绘制
  • 分块
  • 光栅化
  • 合成

Untitled

一个完整的渲染流程大致可总结为如下:

  • 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  • 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

总结:

  • 浏览器不能直接理解 HTML 数据,所以第一步需要将其转换为浏览器能够理解的 DOM 树结构;
  • 生成 DOM 树后,还需要根据 CSS 样式表,来计算出 DOM 树所有节点的样式;
  • 最后计算 DOM 元素的布局信息,使其都保存在布局树中。

2、浏览器中的JavaScript执行机制

JavaScript的执行机制:先编译,再执行。

编译时会创建执行上下文,和可执行区域代码,执行时,先执行执行上下文,然后执行可执行区域代码。

2.1、变量提升

所谓的变量提升,是指在 JavaScript 代码执行过程(编译阶段)中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。

变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段

Untitled

JavaScript代码的执行流程:

  • 编译阶段

代码可分为变量提升部分的代码和执行部分的代码

在执行一段 JavaScript 代码之前,会编译代码,并将代码中的函数和变量保存到执行上下文的变量环境中,其余具体内容会加载到可执行区域。

经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码

执行上下文是 JavaScript 执行一段代码时的运行环境。

Untitled

  • 执行阶段

JavaScript执行时,先执行执行上下文,再执行可执行区域代码,可执行区域代码按顺序执行

总结:

  • JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

2.2、调用栈

调用栈是一种用来管理执行上下文的数据结构。

当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。

当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,将其压入栈顶;函数执行结束之后,出栈,创建的函数执行上下文会被销毁。

JavaScript 引擎利用结构来管理执行上下文,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈,是JavaScript引擎跟踪函数执行的一个机制。

注意事项:

  • 栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。

总结:

  • 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
  • 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
  • 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。

2.3、块级作用域

作用域:

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

块级作用域是通过词法环境的栈结构来实现的

变量提升是通过变量环境来实现的

function foo(){
 var a = 1
 let b = 2
 {
  let b = 3
  var c = 4
  let d = 5
  console.log(a)
  console.log(b)
 }
 console.log(b)
 console.log(c)
 console.log(d)
}
foo()

支持块级作用域:

  • 第一步是编译并创建执行上下文
    • 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
    • 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
    • 在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。
  • 第二步继续执行代码

Untitled

var的创建和初始化被提升,赋值不会被提升; let的创建被提升,初始化和赋值不会被提升; function的创建、初始化和赋值均会被提升。

总结:

  1. var声明的变量进入变量环境
  2. let、const声明的变量进入词法环境
  3. 变量查找顺序是:词法环境--变量环境
  4. var变量提升:最近的函数作用域--全局作用域
  5. let、const变量“提升”:块级作用域--函数作用域--全局作用域(提升表现为中断变量查找顺序)

2.4、作用域链和闭包

由内向外,通过作用域链来查找正确的变量地址

产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。

总结:

  • 把通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构
  • 通过作用域链来查找变量的
  • 闭包保存在堆内存中

2.5、this指向

执行上下文中包含了变量环境、词法环境、外部环境和this

Untitled

执行上下文主要分为三种

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文

所以对应的 this 也只有这三种——【全局执行上下文中的 this】、【函数执行上下文中的 this 】和 【eval 中的 this】

【全局执行上下文中的 this】

全局执行上下文中的this指向window对象

【函数中的 this】

可以使用函数的call、apply、bind方法改变this的指向

1、使用函数的call方法设置this

let guy = {
 name: 'Silly'
}
function getName() {
 this.name = 'SillyBoy'
}
getName.call(guy)
console.log('guy', guy)

// guy {name: 'SillyBoy'}

2、使用对象调用函数来设置this

const obj = {
 name: 'Silly',
 getName: function() {
  console.log('谁', this)
 }
}
obj.getName()

// 谁 {name: 'Silly', getName: ƒ}

使用对象来调用内部的某个函数,该函数内执行上下文的this会指向该调用者本身。相当于执行了obj.getName.call(obj)

3、在构造函数中设置this

function Person() {
 this.name = 'Silly'
}
const xiaoming = new Person()
console.log('构造函数实例', xiaoming)

// 构造函数实例 Person {name: 'Silly'}

当执行 new Person() 的时候,JavaScript 引擎做了如下四件事:

  • 首先创建了一个空对象 xiaoming;
  • 接着调用 Person.call 方法,并将 xiaoming 作为 call 方法的参数,这样当 Person 的执行上下文创建时,它的 this 就指向了 xiaoming 对象;
  • 然后执行 Person 函数,此时的 Person 函数执行上下文中的 this 指向了 xiaoming 对象;
  • 最后返回 xiaoming 对象

嵌套函数中的 this 不会继承外层函数的 this 值,可以手动设置嵌套函数中的this为外层函数中的this

const obj = {
 name: 'Silly',
 getName: function() {
  var self = this
  function bar() {
   self.name = "self是个好东西"
  }
  bar()
  console.log('谁', this)
 }
}
obj.getName()

// 谁 {name: 'self是个好东西', getName: ƒ}

箭头函数没有执行上下文,自然也就没有this,this取决于它的外部函数。

【eval中的 this】

3、V8工作原理/JS内存机制

3.1、栈空间和堆空间

在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。

Untitled

其中的代码空间主要是存储可执行代码的;栈空间就是调用栈,是用来存储执行上下文的;

原始类型的数据是存放在栈中,引用类型的数据是存放在堆中。JavaScript 的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据

3.2、垃圾回收

原始类型数据保存在占内存中,引用类型数据保存在堆内存中

栈垃圾回收

堆垃圾回收

记录当前执行状态的指针称为:ESP(extends stack pointer)

ESP的下移操作就是销毁该函数执行上下文的过程

代际假说:

  • 大部分对象一经分配内存,通常其存活时间较短
  • 很少的不死的对象,内存会存活很久

V8 把堆分成两个区域——新生代和老生代

新生代:存放生存时间短的对象,容量小,副垃圾回收器回收老生代:存放时间长的对象,容量大,主垃圾回收器回收。

老生代:存放时间长的对象,容量大,由主垃圾回收器回收

Untitled

V8 使用副垃圾回收器和主垃圾回收器处理垃圾回收

垃圾回收器工作流程:

  1. 标记空间中活动对象非活动对象
  2. 回收非活动对象所占的内存
  3. 内存整理

副垃圾回收器

新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域

主垃圾回收器

采用标记 - 清除(Mark-Sweep)算法进行垃圾回收。缺点:会产生碎片空间

标记阶段:

从根元素开始,遍历整个DOM树(遍历调用栈),判断里面是否有对这个对象的引用,有则是活动对象,无则标记为垃圾数据

清除阶段:

直接对可回收对象进行清理,清除掉被标记为垃圾数据的内存

Untitled

采用标记 - 整理(Mark-Compact)算法进行垃圾回收。

清除阶段:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

Untitled

由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)

垃圾回收会占用浏览器主进程,为降低老生代可能造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。

Untitled

总结:

堆垃圾回收 一、代际假说 1、大部分对象存活时间很短 2、不被销毁的对象,会活的更久

二、分类 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

三、新生代 算法:Scavenge 算法 原理: 1、把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。 2、新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。 3、先对对象区域中的垃圾做标记,标记完成之后,把这些存活的对象复制到空闲区域中 4、完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。 对象晋升策略: 经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

四、老生代 算法:标记 - 清除(Mark-Sweep)算法 原理: 1、标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。 2、清除:将垃圾数据进行清除。 碎片: 对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。

算法:标记 - 整理(Mark-Compact)算法 原理: 1、标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象。 2、整理:让所有存活的对象都向内存的一端移动 3、清除:清理掉端边界以外的内存

优化算法:增量标记(Incremental Marking)算法 原理: 1、为了降低老生代的垃圾回收而造成的卡顿 2、V8把一个完整的垃圾回收任务拆分为很多小的任务 1、让垃圾回收标记和 JavaScript 应用逻辑交替进行

3.3、编译器和解释器

要深入理解V8的工作原理,要先搞清楚一些概念。编译器、解释器、抽象语法树(AST)、字节码(Bytecode)、即时编译器(JIT)等

编译器(Complier)

编译器TurboFan => 涡轮增压

编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。常见的编译型语言有C/C++、GO等

Untitled

解释器(Interpreter)

解释器lgnition => 点火器

解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。常见的编译型语言有JavaScript和Python等

Untitled

解释器根据AST生成字节码,然后解释器解释并执行字节码

抽象语法树(AST)

AST是一种非常重要的数据结构。

生成AST的流程:

  • 第一阶段:分词(tokenize),即词法分析。

    作用是将一行行的源码拆解成一个个不可再分的token

  • 第二阶段:解析(parse),即语法分析。

    作用是将上一步生成的 token 数据,根据语法规则转为 AST

https://static001.geekbang.org/resource/image/73/36/7320526ef14d974be8393effcf25b436.png?wh=1142*1644

字节码(Bytecode)

字节码就是介于 AST 和机器码之间的一种代码。

但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

优点:使用字节码可以减少系统的内存使用。

在解释执行字节码的过程中,如果发现了某一段代码会被重复多次执行,那么监控机器人就会将这段代码标记为热点代码。

当某段代码被标记为热点代码后,V8 就会将这段字节码丢给编译器,编译器会将字节码编译为二进制代码,然后再对二进制代码执行优化操作,优化后的机器代码执行效率会得到大幅提升。如果下面再执行到这段代码, V8 会优先选择优化后的机器代码,这样代码的执行速度就会大幅提升。

即时编译(JIT)

指字节码配合解释器和编译器的技术,即混合编译执行和解释执行。

两种方法各有优缺点:

  • 解释执行的启动速度快,但是执行时的速度慢。
  • 而编译执行的启动速度慢,但是执行时的速度快。

Untitled

因此:编译器的执行效率随着执行时间的增加而越来越高

总结:

V8 依据 JavaScript 代码生成 AST 和执行上下文,再基于 AST 生成字节码,然后通过解释器执行字节码,通过编译器来优化编译字节码

V8 摒弃了导致 JavaScript 执行速度过慢的解释执行方式,率先采用了即时编译(JIT)的双轮驱动的设计,混合了编译执行和解释执行两种方式。JIT 作为一种权衡策略,大幅优化了 JavaScript 代码的执行效率,也将 JavaScript 虚拟机技术推向了一个全新的高度。

JavaScript性能的优化,得益于对解释器和编译器的不断改进和优化

流程总结

初始化环境;

解析代码生成 AST 和作用域;

依据 AST 和作用域生成字节码;

解释执行字节码;

监听热点代码;

优化热点代码为二进制机器代码;

反优化二进制机器代码。

4、浏览器中的页面循环系统

4.1、消息队列和事件循环

浏览器页面是由消息队列和事件循环系统来驱动的。

消息队列存在在渲染主线程,是一种数据结构,可以存放要执行的任务。

渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。

子线程直接将事件加入消息队列 其它进程通过io线程将事件加入消息队列

Untitled

每个宏任务中有个微任务队列,但是要等到宏任务的主要功能都执行完之后,才去执行队列中的微任务。

循环系统流程:

  1. 添加一个消息队列;
  2. IO 线程中产生的新任务添加进消息队列尾部;
  3. 渲染主线程会循环地从消息队列头部中读取任务,执行任务。

总结:

  • 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。
  • 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
  • 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
  • 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
  • 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。

4.2、WebAPI:setTimeout是如何实现的

消息队列除了宏任务消息队列,每个宏任务下都会维护一个微任务队列。

除此之外,还有有一个延迟任务队列(其实是一个hashmap结构)。可以认为是异步回调队列。

setTimeout是宏任务,延迟队列也是宏任务。

chromium中,当执行一个宏任务时,才会创建微任务队列,等遇到checkpoint时就会执行微任务!微任务是在宏任务快要执行结束之前执行的

注意事项:

  1. 如果当前任务执行时间过久,会影响定时器任务的执行
  2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒(超过5层)
  3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
  4. 延时时间有最大限制(32bit)
  5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉

总结:

setTimeout 是直接将延迟任务添加到延迟队列中

  • 首先,为了支持定时器的实现,浏览器增加了延时队列。
  • 其次,由于消息队列排队和一些系统级别的限制,通过 setTimeout 设置的回调任务并非总是可以实时地被执行,这样就不能满足一些实时性要求较高的需求了。
  • 最后,在定时器中使用过程中,还存在一些陷阱,需要你多加注意。

4.3、WebAPI:XMLHttpRequest是如何实现的

回调函数

将一个函数作为参数传递给另一个函数,作为参数的函数就称为回调函数。回调函数有同步回调和异步回调

同步回调

回调函数在主函数返回之前执行

异步回调

回调函数在主函数外部执行

渲染进程会将请求发送给网络进程,然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程;渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中

总结:

4.4、宏任务和微任务

WHATWG规范

页面中的大部分任务都是在主线程上执行,为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制。

在主线程的循环系统中,可以有多个消息队列,比如鼠标事件的队列,IO完成消息队列,渲染任务队列,并且可以给这些消息队列排优先级。

消息队列中的任务是通过事件循环系统来执行的。

渲染进程内部会维护多个消息队列,主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务

宏任务

消息队列中的任务称为宏任务。

宏任务的执行时长=宏任务的执行时间+全部微任务的执行时间

微任务

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

js执行时,v8会创建一个全局执行上下文,在创建上下文的同时,v8也会在内部创建一个微任务队列,在当前宏任务执行的过程,有时候会产生多个微任务,新创建的微任务会添加到当前微任务队列中。

在当前js即将执行完时、js引擎准备退出执行上下文、清空本次调用栈时,此时引擎会检查微任务队列列表,然后按照顺序执行任务

MutationObserver

一套监听DOM变化的策略,MutationObserver 采用了“异步 + 微任务”的策略。

通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题。

常见宏任务微任务

任务 宏/微任务
事件 宏任务
网络请求 宏任务
定时器 宏任务
fs读取文件 宏任务
Promise.then() 微任务
async/await 微任务
MutationObserver 微任务

总结:

事件循环执行顺序:

  • 先执行同步代码
  • 遇到异步宏任务则将异步宏任务放入宏任务队列中
  • 遇到异步微任务则将异步微任务放入微任务队列中
  • 当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行
  • 微任务执行完毕后再将异步宏任务从队列中调入主线程执行
  • 一直循环直至所有任务执行完毕。

4.5、Promise

Promise 解决的是异步编码风格的问题

Promise 之所以要使用微任务是由 Promise 回调函数延迟绑定技术导致的。

需要异步来提高效率,需要微任务来提高实效性。

总结:

使用 Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显

创建 Promise 时,并不会生成微任务,而是需要等到Promise 对象调用 resolve 或者 reject 函数时,才会产生微任务。产生的微任务并不会立即执行,而是等待当前宏任务快要执行结束时再执行。

4.6、async/await

async/awiat的一大亮点就是使用同步编程的方式实异步逻辑。

ES7引入了async/await,这是 JavaScript 异步编程的一个重大改进,其提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。

使用async/await不仅能让代码更加整洁美观,而且还能确保该函数始终都能返回 Promise。

async/await机制使用的是Promise和Generator技术,更底层一点就是微任务和协程应用。 async/await = promise + 生成器应用 = 微任务 + 协程应用

Generator生成器

生成器函数是一个带星号“*”的函数,可以暂停执行和恢复执行。

如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。。

Generator的底层实现机制是协程(Coroutine)

生成器函数的使用方法

  • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行
  • 外部函数可以通过调用生成器函数的next方法来恢复执行生成器函数
  • 协程在执行期间,遇到了 return 关键字,则终止函数,并将return后面的内容返回给父协程

通常把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器。通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码

协程

一个进程有多个线程,一个线程有多个协程。

一个进程中可以有多个线程并行执行,但是一个线程中不可以有多个协程同时执行。

若想在当前协程未执行完时切换执行其他协程,需要将主线程的控制权交给其他协程。

线程是由CPU进行调度的,CPU的一个时间片内只执行一个线程上下文内的线程,上下文切换过程会比较消耗性能,所以应该减少上下文切换的发生。

协程不被操作系统内核所管理,而完全由程序所控制,一定程度上讲,在切换协程时不太消耗性能。

线程的执行是在内核态,是由操作系统来控制;协程的执行是在用户态,是完全由程序来进行控制,通过调用生成器的next()方法可以让该协程执行,通过yield关键字可以让该协程暂停,交出主线程控制权,通过return 关键字可以让该协程结束。

协程的本质就是在一个线程中维护了两个调用栈,线程可以在这两个调用栈之间进行切换,而且因为这两个调用栈是同时存在的,但是不会同时活动,所以他们之间的通信非常方便。 同时,协程的创建和回收成本不会很高,所以我们在使用协程进行任务协调的时候,并不需要使用池技术

async

async返回一个resolve状态的Promise

await

在执行await时,会创建一个新的Promise,JS引擎会把此微任务放入到微任务队列中,此协程会暂停执行,线程控制权会交回到父协程手中。

父协程会调用Promise.then()监听任务状态,然后继续执行父协程的流程

总结:

Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。

5、浏览器中的页面

5.1、Chrome开发者工具

总结:

5.2、DOM树

HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据

网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据

总结:

CSS和JavaScript都会影响到DOM的生成,JavaScript会阻塞DOM生成,而CSS样式文件又会阻塞 JavaScript的执行

5.3、渲染流水线

总结:

css本身并不会阻断DOM构建,但是结合js脚本就可能会阻塞DOM构建以及首屏加载

5.4、分层和合成机制

每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒更新 60 张图片,更新的图片都来自于显卡中一个叫前缓冲区的地方。

显卡的职责就是合成新的图像,并将图像保存到后缓冲区中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器能读取到最新显卡合成的图像。

为解决卡顿问题,浏览器引入了分层和合成机制。

JS执行动画会占用渲染主线程,而CSS操作动画如果帧的生成方式是合成方式,那么直接在合成线程操作就行,不会引起渲染主线程阻塞

总结:

通常渲染引擎生成一帧图像有三种方式:重排、重绘和合成。

其中重排和重绘操作都是在渲染进程的主线程上执行的,比较耗时;而合成操作是在渲染进程的合成线程上执行的,执行速度快,且不占用主线程。

浏览器的合成,其技术细节主要可以使用三个词来概括:分层、分块和合成。

5.5、页面性能

并非所有的资源都会阻塞页面的首次绘制,比如图片、音频、视频等文件就不会阻塞页面的首次渲染;而 JavaScript、首次请求的HTML资源文件、CSS文件是会阻塞首次渲染的,因为在构建DOM的过程中需要HTML和JavaScript文件,在构造渲染树的过程中需要用到CSS文件。

影响性能的因素

  • 加载阶段:网络和js脚本
  • 交互阶段:js脚本
  • 关闭阶段

阻塞网页首次渲染的资源

  • 关键资源(HTML、JavaScript、CSS)的个数
  • 关键资源的大小
  • 关键资源的传输往返时延(RTT)

页面性能优化原则:减少关键资源个数,降低关键资源大小,降低关键资源的RTT次数。

总结:

在加载阶段,核心的优化原则是

  • 优化关键资源的加载速度
  • 减少关键资源的个数
  • 降低关键资源的 RTT 次数

在交互阶段,核心的优化原则是

  • 尽量减少一帧的生成时间。可以通过减少单次 JavaScript 的执行时间、避免强制同步布局、避免布局抖动、尽量采用 CSS 的合成动画、避免频繁的垃圾回收等方式来减少一帧生成的时长。

5.6、虚拟DOM

浏览器操作DOM不是看起来的那么简单,是会影响到整个渲染流水线的。如果对 DOM 操作不当,可能会引起重排、重绘等操作,甚至还会触发强制同步布局和布局抖动,引出性能问题。

为提高浏览器渲染性能和执行效率,引入虚拟DOM的技术,当执行某操作数据变动时,将变动应用到虚拟DOM中,而不是直接去更改真实DOM,待变动完成后,利用diff算法对比真实DOM和虚拟DOM,找出变动处,并替换渲染到真实DOM中,以此达到提高性能的目的。

其本质是以js运算性能的损耗来换取操作DOM的性能损耗。

双缓存是一种经典的思路,应用在很多场合,能解决页面无效刷新和闪屏的问题,虚拟 DOM 就是双缓存思想的一种体现。

总结:

JavaScript和页面渲染流水线的任务都是在页面主线程上执行,为提高浏览器渲染性能和执行效率,引入虚拟DOM的技术,其本质是以js运算性能的损耗来换取操作DOM的性能损耗。

5.7、渐进式网页应用(PWA)

Progressive Web App

它是一套理念,渐进式增强 Web 的优势,并通过技术手段渐进式缩短和本地应用或者小程序的距离。基于这套理念之下的技术都可以归类到 PWA。

总结:

PWA是由很多技术组成的一个理念,其核心思想是渐进式。对于技术本身而言,它是渐进式演进,逐渐将 Web 技术发挥到极致的同时,也逐渐缩小和本地应用的差距

5.8、WebComponent

组件化的原则:对内高内聚,对外低耦合

WebComponent 是一套技术的组合,具体涉及到了 Custom elements(自定义元素)、Shadow DOM(影子 DOM)和HTML templates(HTML 模板)。WebComponent提供了对局部视图封装能力,可以让DOM、CSSOM和JavaScript运行在局部环境中,这样就使得局部的CSS和DOM不会影响到全局。

shadowDom

shadowDom的作用是将模板中的内容与全局 DOM 和 CSS 进行隔离,这样就可以实现元素和样式的私有化。(shadowDom的JavaScript 脚本不会被隔离)

内部的样式和元素不会影响到全局的样式和元素,在全局环境下,要访问影子 DOM 内部的样式或者元素需要通过约定好的接口来实现

总结:

6、浏览器中的网络

HTTP 是一种允许浏览器向服务器获取资源的协议,是 Web 的基础。

HTTP协议和TCP协议,都是TCP/IP协议簇的子集。HTTP协议属于应用层,TCP协议属于传输层,HTTP协议基于TCP协议,位于TCP协议的上层。

6.1、HTTP/1:HTTP性能优化

HTTP/0.9

HTTP/0.9最早于1991年诞生,其目的很简单,就是为了在网络之间传递HTML超文本内容,所以被称作超文本传输协议。

HTTP/0.9的需求很简单,只用来传输体积很小的HTML文件,所以其特点为:

  • 请求只有请求行,没有请求头请求体
  • 服务器也没有返回头信息
  • 返回的文件内容是以ASCII字符流来传输的

HTTP/1.0

HTTP1.0引入了:状态码、缓存机制、用户代理信息

HTTP/1.0的方案是通过请求头和响应头来进行协商,规定需要的期望接收的数据类型、编码方式、压缩方式、语言等。

服务器接收到浏览器发送过来的请求头信息之后,会根据请求头的信息来准备响应数据。

accept: text/html
accept-encoding: gzip, deflate, br
accept-Charset: ISO-8859-1,utf-8
accept-language: zh-CN,zh

最终浏览器需要根据服务器返回的响应头的信息来处理数据。

content-encoding: brcontent-type: text/html; charset=UTF-8

HTTP/1.1

引入了 Cookie、虚拟主机的支持、对动态内容的支持等特性

HTTP/1.1 中增加了持久连接的方法,它的特点是在一个TCP连接上可以传输多个HTTP请求,只要浏览器或者服务器没有明确断开连接,那么该TCP连接会一直保持。

目前浏览器中对于同一个域名,默认允许同时建立6个TCP持久连接。

这6个TCP连接过程是并行的,每个单独的TCP连接中,可以处理多个HTTP请求。在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。这意味着我们不能随意在一个管道中发送请求和接收内容。

优化网络效率

  • 增加持久连接
  • 浏览器为每个域名最多同时维持6个TCP持久连接
  • 使用CDN实现域名分片机制

总结:

渲染进程有个主线程,DOM解析,样式计算,执行JavaScript,执行垃圾回收等等操作都是在这个主线程上执行的

6.2、HTTP/2:如何提升网络速度

多路复用机制

HTTP/2 使用了多路复用技术,可以将请求分成一帧一帧的数据,实现资源的并行传输。

这样带来了一个额外的好处,就是当收到一个优先级高的请求时,比如接收到 JavaScript 或者 CSS 关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。

通过引入二进制分帧层,就实现了 HTTP 的多路复用技术

Untitled

HTTP2.0相较于HTTP1.1,都是基于TCP的,通信语言没有变化,只是改变了传输方式

特性

  • 支持设置请求的优先级
  • 服务器推送
  • 请求头压缩

总结:

影响HTTP1.1效率的因素:

  • TCP的慢启动
  • 多条TCP连接竞争带宽
  • 队头阻塞

HTTP2.0采用多路复用机制解决HTTP1.1的问题,多路复用是通过在协议栈中添加二进制分帧层来实现的,有了二进制分帧层还能够实现请求的优先级、服务器推送、头部压缩等特性,从而大大提升了文件传输效率。

6.3、HTTP/3:甩掉TCP、TLS的包袱,构建高效网络

HTTP2的缺陷

  • TCP队头阻塞
  • TCP建立连接的延时

TCP队头阻塞

在 TCP 传输过程中,由于单个数据包的丢失而造成的阻塞称为 TCP 上的队头阻塞。

HTTP/2解决了 HTTP/1.1 请求层面的队首阻塞问题,仍然存在 TCP 队首阻塞的问题。以为在HTTP2中,多个请求是跑在同一条TCP通道中的。

TCP建立连接的延时

网络延迟又称为RTT(Round Trip Time)

我们把从浏览器发送一个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间称为RTT(如下图)。RTT是反映网络性能的一个重要指标。

Untitled

HTTP/1 和 HTTP/2 都是使用 TCP 协议来传输,在建立TCP连接时,会进行三次握手,此过程便会消耗掉1.5个RTT。

使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而使用 TLS 也需要一个握手过程,这样就需要有两个握手延迟过程。

在传输数据之前,大概会花掉三到四个RTT,RTT所耗时长会根据浏览器和服务器距离的远近而波动(30ms-100ms),所以若服务器较远,请求过程会消耗300-400ms,很慢了

QUIC协议

基于 UDP 实现类似于 TCP 的多路数据流、传输可靠性等功能,我们把这套功能称为 QUIC 协议

功能特点:

  • 实现了类似 TCP 的流量控制、传输可靠性的功能
  • 集成了 TLS 加密功能
  • 实现了 HTTP/2 中的多路复用功能
  • 实现了快速握手功能

QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。

因为QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接

HTTP1请求过程

Untitled

HTTP2请求过程

Untitled

HTTP3请求过程

Untitled

HTTP2和HTTP3的协议栈

Untitled

总结:

HTTP/3是基于 QUIC 协议的,可以把QUIC看成是集成了UDP+HTTP/2 的多路复用+TLS 等功能的一套协议

HTTP3面临的挑战

  • 目前服务器端和浏览器端都未对HTTP3提供比较完整的支持
  • 部署HTTP3也存在很大问题,因为系统内核对UDP的优化程度远不如对TCP的优化
  • 中间设备僵化问题,一些设备对UDP的优化程度远低于TCP,丢包率较大

7、浏览器安全

浏览器安全可以分为三部分

  • Web 页面安全
  • 浏览器网络安全
  • 浏览器系统安全

7.1、同源策略

定义:若两个URL的协议、域名和端口都相同的话,就称两个URL同源

如以下两个 URL,具有相同的协议https、相同的域名xiachaohiu.com,以及相同的端口443,所以这两个 URL 是同源的

https://xiachaohiu.com?name=sillyboy
https://xiachaohiu.com?name=sillygirl

浏览器默认两个相同的源之间可以相互访问资源和操作DOM,两个不同的源之间若想要相互访问资源或者操作DOM,会有一套基础的安全策略的制约,此策略称为同源策略。

同源策略主要表现:

  • 限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作
  • 限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据
  • 限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点

总结:

同源策略会隔离不同源的DOM、页面数据和网络通信,进而实现Web页面的安全性

内容安全策略CSP

为解决允许嵌入第三方资源的风险(最典型的就是跨站脚本攻击XSS),浏览器引入了内容安全策略,即CSP,由服务端来决定可以加载哪些第三方资源。

跨域资源共享机制CORS

跨域资源共享(Cross Origin Resource Sharing)解决访问不同源其他服务资源的问题,可以进行跨域访问控制,从而使跨域数据传输得以安全进行。

跨文档消息机制

可以通过window.postMessage的JavaScript接口来和不同源的 DOM 进行通信。

7.2、跨站脚本攻击(XSS)

XSS(Cross Site Scripting)攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

可能造成的危害

  • 窃取Cookie信息
  • 监听用户行为
  • 修改DOM或植入恶意DOM

常见的XSS脚本注入方式

  • 存储型XSS攻击
  • 反射型XSS攻击
  • 基于DOM的XSS攻击

存储型XSS攻击

黑客将恶意的脚本注入到用户服务器,用户访问页面请求数据时,就会自动执行该恶意脚本,脚本可能会将获取到的用户数据上传到黑客服务器,从事危害用户的行为。

例如:在表单内容中填入一段script标签,提交后,用户再次访问是就会执行该部分恶意代码

反射型XSS攻击

用户将一段含有恶意代码的请求提交给 Web 服务器,Web 服务器接收到请求时,并不做存储操作,又将恶意代码反射给了浏览器端,这就是反射型 XSS 攻击。

例如:用户把一段包含恶意代码的请求提交到服务器,服务器接收到请求后又将恶意代码反射给浏览器,浏览器将会执行此部分恶意代码(未知的恶意链接)

基于DOM的XSS攻击

基于 DOM 的 XSS 攻击不牵涉到页面 Web 服务器,是通过各种手段在Web资源传输过程或者在用户使用页面的过程中注入恶意脚本修改 Web 页面的数据

阻止XSS攻击

存储型 XSS 攻击和反射型 XSS 攻击都需要经过 Web 服务器来处理,因此这两种类型的漏洞属于服务端的安全漏洞;基于DOM的 XSS 攻击全部都是在浏览器端完成,因此基于 DOM 的 XSS 攻击是属于前端的安全漏洞。

但凡攻击,不外乎脏东西自外而入,故可以此为切入点,开展措施,防范不干净的东西(脚本、链接等)

常用的阻击XSS攻击策略

  • 服务器对输入性的脚本内容进行过滤或转码
  • 利用CSP
    • 限制加载其他域下的资源文件
    • 禁止向第三方域提交数据
    • 禁止执行内联脚本和未授权的脚本
  • 使用httpOnly属性
    • 将某些 Cookie 设置为 HttpOnly 标志,则Cookie将只能使用在 HTTP 请求过程中

总结:

XSS 攻击就是黑客往页面中注入恶意脚本,然后将页面的一些重要数据上传到恶意服务器。常见的三种 XSS 攻击模式是存储型 XSS 攻击、反射型 XSS 攻击和基于 DOM 的 XSS 攻击

这三种攻击方式的共同点是都需要往用户的页面中注入恶意脚本,然后再通过恶意脚本将用户数据上传到黑客的恶意服务器上。而三者的不同点在于注入的方式不一样,有通过服务器漏洞来进行注入的,还有在客户端直接注入的。

针对这些 XSS 攻击,主要有三种防范策略,第一种是通过服务器对输入的内容进行过滤或者转码,第二种是充分利用好 CSP,第三种是使用 HttpOnly 来保护重要的 Cookie 信息。

另外还可以在根源上杜绝恶意脚本的注入,例如在前端输入内容时就进行数据过滤和拦截,以及提交数据发送请求时提供验证码等等

7.3、CSRF攻击

CSRF(Cross-site request forgery)跨站请求伪造攻击,指黑客引诱用户打开恶意网站,利用用户的登录状态发起的跨站请求。

常见的CSRF攻击方式

  • 自动发起GET请求
  • 自动发起POST请求
  • 引诱用户点击未知链接

防范CRSF攻击,主要的防护手段是提升服务器的安全性

  • 利用好Cookie的SameSite属性
  • 在服务器端验证请求来源的站点
  • CSRF Token

总结:

要发起 CSRF 攻击需要具备三个条件

  • 目标站点存在漏洞
  • 用户要登录过目标站点
  • 黑客需要通过第三方站点发起攻击

防止 CSRF 攻击主要方式

  • 充分利用好 Cookie 的 SameSite 属性
  • 验证请求的来源站点
  • 使用 CSRF Token

页面安全问题的主要原因就是浏览器为同源策略开的两个“后门”:一个是在页面中可以任意引用第三方资源,另外一个是通过 CORS 策略让 XMLHttpRequest 和 Fetch 去跨域请求资源。

HttpOnly:Js无法读取cookie,无法在XMLHttpRequest或fetch中使用

SameSite: 第三方链接发起的请求不携带目标站点的cookie

Oringin:请求发起的站点,可以正常携带cookie由服务器判断发起请求的站点是否合法

CSRF Token: 生成一个token字符串在页面中返回(Dom)

7.4、安全沙箱

由于渲染进程需要执行 DOM 解析、CSS 解析、网络图片解码等操作,如果渲染进程中存在系统级别的漏洞,那么以上操作就有可能让恶意的站点获取到渲染进程的控制权限,进而又获取操作系统的控制权限,这对于用户来说是非常危险的。

将渲染进程和操作系统隔离的这道墙就是安全沙箱。浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程。

安全沙箱最小的保护单位是进程,并且能限制进程对操作系统资源的访问和修改。

持久存储

读写文件的操作全部放在了浏览器内核中实现,然后通过 IPC 将操作结果转发给渲染进程

网络访问

渲染进程通过浏览器内核发起网络请求,并检查渲染进程是否有权限请求某URL

用户交互

为了限制渲染进程监控到用户的输入事件,所以为每个渲染进程都有安全沙箱保护,与用户的交互需要通过浏览器内核由IPC转发给渲染进程

站点隔离

将标签级的渲染进程重构为 iframe 级的渲染进程,然后严格按照同一站点的策略来分配渲染进程,就是 Chrome 中的站点隔离

总结:

安全沙箱的目的是隔离渲染进程和操作系统,让渲染进行没有访问操作系统的权利。

在多进程的基础之上引入了安全沙箱,有了安全沙箱,就可以将操作系统和渲染进程进行隔离,这样即便渲染进程由于漏洞被攻击,也不会影响到操作系统的。

由于渲染进程采用了安全沙箱,所以在渲染进程内部不能与操作系统直接交互,于是就在浏览器内核中实现了持久存储、网络访问和用户交互等一系列与操作系统交互的功能,然后通过 IPC 和渲染进程进行交互。

7.5、HTTPS:让数据传输更安全

HTTP是应用层,从 HTTP 协议栈层面来看,可以在 TCP 和 HTTP 之间插入一个安全层(SSL/TLS),所有经过安全层的数据都会被加密或者解密。

Untitled

安全层有两个主要的职责

  • 对发起 HTTP 请求的数据进行加密操作
  • 对接收到 HTTP 的内容进行解密操作

加解密

在HTTPS 发送数据之前,浏览器和服务器之间需要协商加解密方式和密钥,这个过程就是HTTPS建立安全连接的过程。

HTTPS第一版:对称加密

浏览器使用对称加密,随机串和加密套件有被泄露的风险,它们生成的秘钥也有泄露的风险。

Untitled

HTTPS第二版:非对称加密

非对称加密算法有 A、B 两把密钥,若用A密钥加密,就得用B密钥解密;若用B密钥加密,则需使用A密钥解密。

在 HTTPS 中,服务器会将其中的一个密钥通过明文的形式发送给浏览器,发送给浏览器的密钥称为公钥,服务器自己留下的那个密钥称为私钥。

Untitled

非对称加密请求过程

  • 浏览器发送加密套件列表给服务器
  • 浏览器选择一个加密套件,将加密套件和用于浏览器加密的公钥发送给浏览器,用于服务器解密的私钥则自己留着
  • 浏览器和服务器确认消息

浏览器经过和服务器协商后,获得加密套件和公钥,使用公钥给数据加密后,发送请求给服务器。服务器接收到请求数据后使用私钥来加解密,传会数据给浏览器,浏览器再使用公钥解密请求来的数据。

非对称加密的问题

  • 加解密速度慢,效率低
  • 无法保证从服务器获取到的数据的安全(公钥可能泄漏)

HTTPS第三版:对称和非对称混合加密

在传输数据阶段依然使用对称加密,但是对称加密的密钥采用非对称加密来传输

Untitled

请求流程

  • 浏览器向服务器发送对称加密套件列表、非对称加密套件列表和随机数 client-random
  • 服务器保存随机数 client-random,选择对称加密和非对称加密的套件,然后生成随机数 service-random,向浏览器发送选择的加密套件、service-random 和公钥
  • 浏览器保存公钥,并生成随机数 pre-master,然后利用公钥对 pre-master 加密,并向服务器发送加密后的数据
  • 服务器拿出自己的私钥,解密出 pre-master 数据,并返回确认消息

服务器和浏览器就有了共同的 client-random、service-random 和 pre-master,然后服务器和浏览器会使用这三组随机数生成对称密钥,因为服务器和浏览器使用同一套方法来生成密钥,所以最终生成的密钥也是相同的

pre-master 是经过公钥加密之后传输的,所以黑客无法获取到 pre-master,这样黑客就无法生成密钥

浏览器使用公钥加密了随机数,可以安全传输到服务端,服务端使用私钥解密得到随机数明文,然后这个随机数可以结合加密套件来生成对称秘钥。 这个对称秘钥就可以拿来传输数据给浏览器,随机数密文既然是安全的(只能被私钥解密),那么服务端使用私钥解密后的随机数生成的对称秘钥也就是安全的,可以用来加密数据。加密数据传给浏览器时就算被截获也无法破解,因为黑客不知道client随机数明文是什么。 当数据发送到客户端,客户端使用自己生成的对称密钥来解密数据。

真正的pre-master需要加密后才传输,黑客拦截的时候无法解密,这样黑客就没办法根据三个随机数生成密钥了。

HTTPS第四版:添加数字证书

数字证书有两个作用,一个是通过数字证书向浏览器证明服务器的身份,另一个是数字证书里面包含了服务器公钥

Untitled

服务器并没有直接返回公钥给浏览器,而是返回了数字证书,而公钥正是包含在数字证书中的;在浏览器端多了一个证书验证的操作,验证了证书之后,才继续后续流程。

通过引入数字证书,实现了服务器的身份认证功能,这样即便黑客伪造了服务器,但是由于证书没有办法伪造的,所以依然无法欺骗用户

数字证书

向CA(Certificate Authority)机构申请领取数字证书(Digital Certificate)

CA证书(digital certificate)包含了:公钥,组织信息,CA信息,有效时间,证书序列号和CA生成的签名等。

申请和使用证书注意事项

  • 申请数字证书是不需要提供私钥的,要确保私钥永远只能由服务器掌握
  • 数字证书最核心的是 CA 使用它的私钥生成的数字签名
  • 内置 CA 对应的证书称为根证书,根证书是最权威的机构,它们自己为自己签名,我们把这称为自签名证书。

总结:

由于 HTTP 的明文传输特性,在传输过程中的每一个环节,数据都有可能被窃取或者篡改,这倒逼着我们需要引入加密机制。于是我们在 HTTP 协议栈的 TCP 和 HTTP 层之间插入了一个安全层,负责数据的加密和解密操作。

我们使用对称加密实现了安全层,但是由于对称加密的密钥需要明文传输,所以我们又将对称加密改造成了非对称加密。但是非对称加密效率低且不能加密服务器到浏览器端的数据,于是我们又继续改造安全层,采用对称加密的方式加密传输数据和非对称加密的方式来传输密钥,这样我们就解决传输效率和两端数据安全传输的问题。

采用这种方式虽然能保证数据的安全传输,但是依然没办法证明服务器是可靠的,于是又引入了数字证书,数字证书是由 CA 签名过的,所以浏览器能够验证该证书的可靠性。

HTTPS的实现过程

  1. 对称加密传输(协商秘钥的过程容易被窃取)
  2. 非对称加密传输(服务端用私钥加密的内容,可以通过它的公钥进行解密)
  3. 非对称加密交换秘钥、对称加密传输内容(DNS劫持 如何保证服务器是可信的)
  4. 引入CA权威机构保证服务器可信性。

HTTPS握手过程:

  1. 建立TCP链接
  2. 获取服务器证书并检查证实真实性
  3. 通过证书里服务器的公钥发送自己的公钥以及协商对称加密需要的信息给服务器.
  4. 服务器返回协商结果
  5. 双方生成对称密钥
  6. 开始通信

数字证书的申请过程:服务器生成一对公钥和私钥,私钥自己保存,通过公钥+企业+网站信息去CA机构申请证书。CA机构通过全方位的验证给这个网站颁发证书,证书内容包括企业信息、证书有效期、证书编号,以及自己私钥加密上述信息的摘要、网站的公钥。服务器就获得了CA的认证。

浏览器认证证书过程:浏览器从服务器拿到网站的证书,通过CA的公钥解密证书信息的摘要跟使用摘要算法计算企业信息等的摘要对比,如果一致则证明证书有效。如果证书CA是可靠的呢,通过给CA颁发证书的根CA验证,通常操作系统中包括顶级CA证书(它们自己给自己签名称为自签名证书,我们自己生成证书也是自签名证书 只是它不是操作系统内置的

因此,HTTPS 并非是绝对安全的,采用 HTTPS 只是加固了城墙的厚度,但是城墙依然有可能被突破。


其他内容

浏览上下文组-渲染进程相关内容

在 WhatWG 规范中,把这一类具有相互连接关系的标签页称为浏览上下文组 (Groupings of browsing contexts)

通常情况下,我们把一个标签页所包含的内容,诸如 window 对象,历史记录,滚动条位置等信息称为浏览上下文。这些通过脚本相互连接起来的浏览上下文就是浏览上下文组。

Chrome 浏览器会将浏览上下文组中属于同一站点的标签分配到同一个渲染进程中

如果两个标签页都位于同一个浏览上下文组,且属于同一站点,那么这两个标签页会被浏览器分配到同一个渲染进程中。如果这两个条件不能同时满足,那么这两个标签页会分别使用不同的渲染进程来渲染。

使用同一个渲染进程需要满足两个条件:首先 A 标签页和 B 标签页属于同一站点,其次 A 标签页和 B 标签页需要有连接关系

为什么Chrome浏览器使用同一站点划分渲染进程,而不是使用同源策略划分渲染进程?

同源要求协议+域名+端口完全相同,而同一站点只要求协议+根域名相同 通常情况下,同一站点便能保障安全性;放开限制条件可以更好的进行资源复用

任务调度系统-消息队列/事件循环

渲染进程内部的大多数任务都是在主线程上执行的,诸如 JavaScript 执行、DOM、CSS、计算布局、V8 的垃圾回收等任务

主线程维护了一个普通的消息队列和一个延迟消息队列,调度模块会按照规则依次取出这两个消息队列中的任务,并在主线程上执行。

单消息队列的队头阻塞问题

新任务被放入到普通消息队列队尾中,渲染主线程会按照先进先出的顺序依次执行消息队列中的任务。

在单消息队列架构下,存在着低优先级任务会阻塞高优先级任务的情况

为了解决队头阻塞问题,引入了多个不同优级的消息队列,并将紧急的任务添加到高优先级队列,基于不同的场景来动态调整消息队列的优先级。给每个队列一个权重,如果连续执行了一定个数的高优先级的任务,那么中间会执行一次低优先级的任务。

当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号(vertical synchronization)给 GPU,简称 VSync。

显示器按照一定频率进行刷新图像,通常是60HZ,即每秒刷新60次,每16.67毫秒刷新一次。

从显卡的缓冲区读取浏览器生成的图像来呈现画面,使用双缓存技术解决图像闪烁问题。 当浏览器生成图像频率和显示器读取图像的频率不一致时,会造成不连贯的问题,具体表现在浏览器生成图像频率慢则卡顿、快则丢帧。 为同步浏览器与显示器的频率,显示器会在读取下一帧图像前,发送垂直同步信号 VSync 给到 Gpu,Gpu给到浏览器进程,浏览器进程通知渲染进程,渲染进程接受到信号后着手准备新一帧图像的生成,收到信号前的空闲阶段则可以处理低优先级任务(垃圾回收等)。 rAF回调函数任务的执行时机为渲染进程接受到 VSync 信号之后,绘制下一帧图像(style计算,layout)之前。

事件循环包含重渲染过程如下: 1.从任务列队中取出宏任务来执行 2.取出宏任务下的所有微任务执行 3.判断页面是否需要渲染,有一个 rendering opportunity 的概念,也就是不一定一次事件循环就会有一次渲染,要根据屏幕刷新率,页面性能,页面是否在后台运行来决定,通常渲染间隔是固定的 以下情况页面不会渲染:1.页面没有视觉上的改变;2.帧动画为空,即没有定义requestAnimationFrame的回调函数 4.对于需要渲染的文档,如果窗口大小发生变化,执行监听的resize方法; 5.对于需要渲染的文档,如果页面发生了滚动,执行 scroll 方法。 6.对于需要渲染的文档,执行requestAnimationFrame回调。 7.对于需要渲染的文档,执行intersectionObserver的回调。 8.对于需要渲染的文档,重新绘制页面展示给用户。 9.判断 task队列和microTask队列是否都为空,如果是的话,则进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数。

页面性能

Web 性能描述了 Web 应用在浏览器上的加载和显示的速度。讨论web性能时应该从页面加载阶段和页面交互阶段来考虑。

Chrome 采集 Web 性能数据的两个工具:Performance 和 LightHouse,Performance 可以采集非常多的性能,但是其使用难度大,相反,LightHouse 就简单了许多,它会分析检测到的性能数据并给出站点的性能得分,同时,还会给我们提供一些优化建议

LightHouse只能监控加载阶段的性能数据,还可以检测代码是否符合一些最佳实践,并给出提示。Performance还可以监控交互阶段的性能数据。

FP、FCP、LCP

在渲染进程确认要渲染当前的请求后,渲染进程会创建一个空白页面,我们把创建空白页面的这个时间点称为 First Paint,简称 FP。

当页面中绘制了第一个像素时,我们把这个时间点称为 First Content Paint,简称 FCP。

当首屏内容完全绘制完成时,我们把这个时间点称为 Largest Content Paint,简称 LCP。

加载过程划分为三个阶段

  • 导航阶段
  • 解析 HTML 文件阶段
  • 生成位图阶段

在导航流程中,主要是处理响应头的数据,并执行一些老页面退出之前的清理操作。

在解析 HTML 数据阶段,主要是解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOM 和 CSSOM。

最后在生成最终显示位图的阶段,主要是将生成的 DOM 和 CSSOM 合并,这包括了布局 (Layout)、分层、绘制、合成等一系列操作。