了解 Google Chrome
Chrome架构:仅仅打开了1个页面,为什么有4个进程?#
线程 VS 进程#
线程是不能单独存在的,它是由进程来启动和管理的。一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。
chrome打开一个标签页:
1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。
- 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
TCP协议:如何保证页面文件能被完整送达浏览器?#
在衡量 Web 页面性能的时候有一个重要的指标叫“FP(First Paint)”,是指从页面加载到首次开始绘制的时长。其中一个重要的因素是网络加载速度。
互联网,实际上是一套理念和协议组成的体系架构。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
tcp对于udp:
- 对于数据包丢失的情况,TCP 提供重传机制;
- TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
HTTP请求流程:为什么很多站点第二次打开速度会很快?#
浏览器端发起 HTTP 请求流程#
- 构建请求
- 查找缓存
览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。
好处:
缓解服务器端压力,提升性能(获取资源的耗时更短了);
对于网站来说,缓存是实现快速资源加载的重要组成部分。 - 准备 IP 地址和端口
HTTP 协议作为应用层协议,用来封装请求的文本信息,TCP/IP 作传输层协议,HTTP 的内容是通过 TCP 的传输数据阶段来实现的
第一步浏览器会请求 DNS 返回域名对应的 IP
当然浏览器还提供了DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。 - 等待 TCP 队列
Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接 - 建立 TCP 连接
- 发送 HTTP 请求
- 请求行: 包含请求方法、请求 URI(Uniform Resource Identifier)和 HTTP 版本协议。
- 请求头
- 请求体
服务器端处理 HTTP 请求流程#
- 返回请求
- 响应行: 包含协议版本和状态码。
- 响应头
- 响应体
- 断开连接
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:Connection:Keep-Alive
那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。 - 重定向
响应行返回的状态码是 301,状态 301 就是告诉浏览器,我需要重定向到另外一个网址,而需要重定向的网址正是包含在响应头的Location
字段中,接下来,浏览器获取Location
字段中的地址,并使用该地址重新导航,这就是一个完整重定向的执行流程。
1. 为什么很多站点第二次打开速度会很快?#
第一次加载页面过程中,缓存了一些耗时的数据,DNS 缓存和页面资源缓存这两块数据是会被浏览器缓存的,通过响应头中的 Cache-Control
字段来设置是否缓存该资源,如果缓存过期使用If-None-Match:"4f80f-13c-3a1xb12a"
发送MD5加密的值和后端数据进行对比
2.如何保持登录状态#
set-cookie
导航流程:从输入URL到页面展示,这中间发生了什么?#
用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。
从输入 URL 到页面展示
- 用户输入
关键字是搜索内容,还是请求的 URL - URL 请求过程
首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。
接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
(1)重定向
301或302 这时网络进程会从响应头的 Location 字段里面读取重定向的地址
(2)响应数据类型处理
Content-Type
是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。
下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。 - 准备渲染进程
Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程 - 提交文档
“提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。 - 渲染阶段
渲染流程:HTML、CSS和JavaScript,是如何变成页面的?#
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
构建 DOM 树#
浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
构建布局树#
布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。
样式计算(Recalculate Style)#
css来源#
- 通过 link 引用的外部 CSS 文件
<style>
标记内的 CSS- 元素的 style 属性内嵌的 CSS
- 把 CSS 转换为浏览器能够理解的结构
当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。 - 转换样式表中的属性值,使其标准化
需要将所有值转换为渲染引擎容易理解的、标准化的计算2em -> 23px
- 计算出 DOM 树中每个节点的具体样式
继承规则,层叠规则。CSS 继承就是每个 DOM 节点都包含有父节点的样式。层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
布局阶段#
那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
- 创建布局树:包含可见元素布局树
- 布局计算
分层#
复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。postion、z-index、filter、opacity
第二点,需要剪裁(clip)的地方也会被创建为图层。
图层绘制#
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
栅格化(raster)操作#
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
根据视口(viewport)的大小,合成线程会将图层划分为图块(tile),合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示#
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上
性能优化#
- 更新了元素的几何属性(重排):会触发重新布局,解析之后的一系列子阶段,重排需要更新完整的渲染流水线,所以开销也是最大的。
- 更新元素的绘制属性(重绘):相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
- 直接合成阶段:只执行合成操作,transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。
V8工作原理#
V8工作原理和V8如何执行JavaScript代码请移步到了解V8
Chrome开发者工具:利用网络面板做性能分析#
Chrome 开发者工具#
网络面板#
- 控制器
- 过滤器
网络面板中的过滤器,主要就是起过滤功能。因为有时候一个页面有太多内容在详细列表区域中展示了,而你可能只想查看 JavaScript 文件或者 CSS 文件,这时候就可以通过过滤器模块来筛选你想要的文件类型。 - 抓图信息
抓图信息区域,可以用来分析用户等待页面加载时间内所看到的内容,分析用户实际的体验情况。比如,如果页面加载 1 秒多之后屏幕截图还是白屏状态,这时候就需要分析是网络还是代码的问题了。(勾选面板上的“Capture screenshots”即可启用屏幕截图。) - 时间线
时间线,主要用来展示 HTTP、HTTPS、WebSocket 加载的状态和时间的一个关系,用于直观感受页面的加载过程。如果是多条竖线堆叠在一起,那说明这些资源被同时被加载。至于具体到每个文件的加载信息,还需要用到下面要讲的详细列表。 - 详细列表
这个区域是最重要的,它详细记录了每个资源从发起请求到完成请求这中间所有过程的状态,以及最终请求完成的数据信息。通过该列表,你就能很容易地去诊断一些网络问题。
详细列表是我们本篇文章介绍的重点,不过内容比较多,所以放到最后去专门介绍了。 - 下载信息概要
下载信息概要中,你要重点关注下 DOMContentLoaded 和 Load 两个事件,以及这两个事件的完成时间。
DOMContentLoaded,这个事件发生后,说明页面已经构建好 DOM 了,这意味着构建 DOM 所需要的 HTML 文件、JavaScript 文件、CSS 文件都已经下载完成了。
Load,说明浏览器已经加载了所有的资源(图像、样式表等)。
通过下载信息概要面板,你可以查看触发这两个事件所花费的时间。
网络面板中的详细列表#
- 列表的属性
- 详细信息
- 单个资源的时间线
- Queuing
- Stalled
- Proxy Negotiation
- Initial connection/SSL
- Request sent
- Waiting (TTFB)第一字节时间
- Content Download
优化时间线上耗时项#
- 排队(Queuing)时间过久
排队时间过久,大概率是由浏览器为每个域名最多维护 6 个连接导致的。那么基于这个原因,你就可以让 1 个站点下面的资源放在多个域名下面,比如放到 3 个域名下面,这样就可以同时支持 18 个连接了,这种方案称为域名分片技术。除了域名分片技术外,我个人还建议你把站点升级到 HTTP2,因为 HTTP2 已经没有每个域名最多维护 6 个 TCP 连接的限制了。 - 第一字节时间(TTFB)时间过久
这可能的原因有如下:
- 服务器生成页面数据的时间过久。对于动态网页来说,服务器收到用户打开一个页面的请求时,首先要从数据库中读取该页面需要的数据,然后把这些数据传入到模板中,模板渲染后,再返回给用户。服务器在处理这个数据的过程中,可能某个环节会出问题。
- 网络的原因。比如使用了低带宽的服务器,或者本来用的是电信的服务器,可联通的网络用户要来访问你的服务器,这样也会拖慢网速。
- 发送请求头时带上了多余的用户信息。比如一些不必要的 Cookie 信息,服务器接收到这些 Cookie 信息之后可能需要对每一项都做处理,这样就加大了服务器的处理时长。
对于这三种问题,你要有针对性地出一些解决方案。面对第一种服务器的问题,你可以想办法去提高服务器的处理速度,比如通过增加各种缓存的技术;针对第二种网络问题,你可以使用 CDN 来缓存一些静态文件;至于第三种,你在发送请求时就去尽可能地减少一些不必要的 Cookie 数据信息。
- Content Download 时间过久
如果单个请求的 Content Download 花费了大量时间,有可能是字节数太多的原因导致的。这时候你就需要减少文件大小,比如压缩、去掉源码中不必要的注释等方法。
DOM树:JavaScript是如何影响DOM树构建的?#
什么是 DOM#
从网络传给渲染引擎的 HTML 文件字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是 DOM。DOM 提供了对 HTML 文档结构化的表述。
DOM 树如何生成#
在渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。
HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。
网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,比如 content-type 的值是“text/html”,那么浏览器就会判断这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器。
第一个阶段,通过分词器将字节流转换为 Token。 Token,分为 Tag Token 和文本 Token。Tag Token 又分 StartTag 和 EndTag。
至于后续的第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。
HTML 解析器维护了一个Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。(这是一个一边压栈一边生成DOM节点的过程 ,文本 Token 不需要压栈)
JavaScript 是如何影响 DOM 生成的#
解析到<script>
标签时,渲染引擎判断这是一段脚本,此时 HTML 解析器就会暂停 DOM 的解析,因为接下来的 JavaScript 可能要修改当前已经生成的 DOM 结构。
如果有src属性,会通过src下载脚本,JavaScript 文件的下载过程会阻塞 DOM 解析。
JavaScript 脚本依赖样式表(.css文件,因为会操作CSSOM),渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。
优化:
- 预解析操作:当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
- 使用 CDN 来加速 JavaScript 文件的加载
- 压缩 JavaScript 文件的体积
- JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码(async加载完成,会立即执行;defer在 DOMContentLoaded 事件之前执行。)
渲染流水线:CSS如何影响首次加载时的白屏时间?#
渲染流水线视角下的 CSS#
- 渲染进程或浏览器进程发起的请求被送到网络进程中去执行。网络进程接收到返回的 HTML 数据之后,将其发送给渲染进程,渲染进程会解析 HTML 数据并构建 DOM。
- DOM 构建结束之后、.css 文件还未下载完成的这段时间内,渲染流水线无事可做,因为下一步是合成布局树,而合成布局树需要 CSSOM 和 DOM,所以这里需要等待 CSS 加载结束并解析成 CSSOM。
那渲染流水线为什么需要 CSSOM 呢?#
渲染引擎无法直接理解 CSS 文件内容,所以需要将其解析成渲染引擎能够理解的结构 CSSOM。CSSOM 具有两个作用,第一个是提供给 JavaScript 操作样式表的能力,第二个是为布局树的合成提供基础的样式信息。(document.styleSheets)
body中有JavaScript脚本(不在body底部)执行流程:
由于JavaScript脚本依赖与CSSOM,所以 CSS 在部分情况下也会阻塞 DOM 的生成。
如果JavaScript脚本有src属性和css文件通过link href属性的执行流程:
影响页面展示的因素以及优化策略#
主要说浏览器渲染进程阶段:解析 HTML、下载 CSS、下载 JavaScript、生成 CSSOM、执行 JavaScript、生成布局树、绘制页面一系列操作。
通常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript。
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
分层和合成机制:为什么CSS动画比JavaScript高效?#
三个词来概括:分层、分块和合成。
显示器是怎么显示图像的#
每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒更新 60 张图片,更新的图片都来自于显卡中一个叫前缓冲区的地方,显示器所做的任务很简单,就是每秒固定读取 60 次前缓冲区中的图像,并将读取的图像显示到显示器上。
显卡的职责就是合成新的图像,并将图像保存到后缓冲区中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器能读取到最新显卡合成的图像。
如何生成一帧图像#
总结为三种方式:
- 重排:速度最慢,需要重新计算布局
- 重绘:直接从绘制开始(次之)
- 合成(优先)
分层和合成(渲染进程分层到合成阶段)#
将素材分解为多个图层的操作就称为分层,最后将这些图层合并到一起的操作就称为合成。所以,分层和合成通常是一起使用的。在 Chrome 的渲染流水线中,分层体现在生成布局树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree),层树是渲染流水线后续流程的基础结构。需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。
分块#
分层是从宏观上提升了渲染效率,那么分块则是从微观层面提升了渲染效率。合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——纹理上传,这是因为从计算机内存上传到 GPU 内存的操作会比较慢。策略:在首次合成图块的时候使用一个低分辨率的图片。
如何利用分层技术优化代码#
使用will-change
,渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。 JavaScript 来写这些效果,会牵涉到整个渲染流水线,这也是 CSS 动画比 JavaScript 动画高效的原因。缺点:需要额外的内存。
.box {
will-change: transform, opacity;
}
页面性能:如何系统地优化页面?#
加载阶段#
能阻塞网页首次渲染的资源称为关键资源如:css文件,JavaScript脚本。基于关键资源,我们可以继续细化出来三个影响页面首次渲染的核心因素。
- 关键资源个数。关键资源个数越多,首次页面的加载时间就会越长。
- 关键资源大小:资源内容越小,资源下载时间越短。
- 请求关键资源需要多少个 RTT(Round Trip Time):TCP 协议传输一个文件时,由于 TCP 的特性,这个数据并不是一次传输到服务端的,而是需要拆分成一个个数据包来回多次进行传输的。RTT 就是这里的往返时延。它是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。通常 1 个 HTTP 的数据包在 14KB 左右,所以 1 个 0.1M 的页面就需要拆分成 8 个包来传输了,也就是说需要 8 个 RTT。(只计算最大的资源的RTT)
针对上述情况优化: - JavaScript 和 CSS 改成内联的形式;JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 sync 或者 defer 属性; CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。(关键资源变为非关键资源)
- 压缩 CSS 和 JavaScript 资源,移除注释内容
- 减少关键资源的个数和减少关键资源的大小搭配来实现;使用 CDN 来减少每次 RTT 时长。
在优化实际的页面加载速度时,你可以先画出优化之前关键资源的图表,然后按照上面优化关键资源的原则去优化,优化完成之后再画出优化之后的关键资源图表。
交互阶段(用户交互)#
交互优化其实就是提高渲染进程渲染帧的速度。
可以从三方面入手:重排、重绘、合成。
- 减少 JavaScript 脚本执行时间(不要霸占太久主线程)
- 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
- 另一种是采用 Web Workers。
- 避免强制同步布局
通过 DOM 接口执行添加元素或者删除元素等操作后,是需要重新计算样式和布局的,不过正常情况下这些操作都是在另外的任务中异步完成的,这样做是为了避免当前的任务占用太长的主线程时间。
// 可以用 Performance 工具来记录添加元素的过程
// 结论:执行 JavaScript 添加元素是在一个任务中执行的,重新计算样式布局是在另外一个任务中执行,这就是正常情况下的布局操作。
<html>
<body>
<div id="mian_div">
<li id="time_li">time</li>
<li>geekbang</li>
</div>
<p id="demo"> 强制布局 demo</p>
<button onclick="foo()"> 添加新元素 </button>
<script>
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
</script>
</body>
</html>
所谓强制同步布局,是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中。
// 上述代码变为同步布局,再用 Performance 工具记录
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
// 由于要获取到 offsetHeight,
// 但是此时的 offsetHeight 还是老的数据,
// 所以需要立即执行布局操作
console.log(main_div.offsetHeight)
}
将新的元素添加到 DOM 之后,我们又调用了main_div.offsetHeight来获取新 main_div 的高度信息。如果要获取到 main_div 的高度,就需要重新布局,所以这里在获取到 main_div 的高度之前,JavaScript 还需要强制让渲染引擎默认执行一次布局操作。我们把这个操作称为强制同步布局。
为了避免强制同步布局,我们可以调整策略,在修改 DOM 之前查询相关值。代码如下所示:
function foo() {
let main_div = document.getElementById("mian_div")
// 为了避免强制同步布局,在修改 DOM 之前查询相关值
console.log(main_div.offsetHeight)
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
- 避免布局抖动
布局抖动,是指在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。
for 循环语句里面不断读取属性值,每次读取属性值之前都要进行计算样式和布局。
function foo() {
let time_li = document.getElementById("time_li")
for (let i = 0; i < 100; i++) {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
new_node.offsetHeight = time_li.offsetHeight;
document.getElementById("mian_div").appendChild(new_node);
}
}
在 foo 函数内部重复执行计算样式和布局,这会大大影响当前函数的执行效率。这种情况的避免方式和强制同步布局一样,都是尽量不要在修改 DOM 结构时再去查询一些相关值。
4. 合理利用 CSS 合成动画(提示:will-change属性)
5. 避免频繁的垃圾回收:尽可能优化储存结构,尽可能避免小颗粒对象的产生。
虚拟DOM:虚拟DOM和实际的DOM有何不同?#
DOM 的缺陷#
引发重排形象地理解就是“牵一发而动全身”,还有可能引发强制同步布局和布局抖动的问题,这些操作都会大大降低渲染效率。
什么是虚拟 DOM#
- 创建阶段。将多次的DOM操作组合在一起,创建出来虚拟 DOM(真实的 DOM 树的结构)由虚拟 DOM 树创建出真实 DOM 树,真实的 DOM 树生成完后,再触发渲染流水线往屏幕输出页面。
- 更新阶段。如果数据发生了改变(很多改变),那么就需要根据新的数据创建一个新的虚拟 DOM 树;然后比较两个树,找出变化的地方,并把变化的地方一次性更新到真实的 DOM 树上;最后渲染引擎更新渲染流水线,并生成新的页面。(React Fiber reconciler 算法,就是在执行算法的过程中出让主线程,这样就解决了 Stack reconciler 函数占用时间过久的问题。)
1. 双缓存#
可以把虚拟 DOM 看成是 DOM 的一个 buffer,和图形显示一样,它会在完成一次完整的操作之后,再把结果应用到 DOM 上,这样就能减少一些不必要的更新,同时还能保证 DOM 的稳定输出。
2. MVC 模式#
核心思想就是将数据和视图分离,根据不同的通信路径和控制器不同的实现方式,基于 MVC 又能衍生出很多其他的模式,如 MVP、MVVM 等。
所以在分析基于 React 或者 Vue 这些前端框架时,我们需要先重点把握大的 MVC 骨架结构,然后再重点查看通信方式和控制器的具体实现方式,这样我们就能从架构的视角来理解这些前端框架了。比如在分析 React 项目时,我们可以把 React 的部分看成是一个 MVC 中的视图,在项目中结合 Redux 就可以构建一个 MVC 的模型结构,如下图所示:
- 图中的控制器是用来监控 DOM 的变化,一旦 DOM 发生变化,控制器便会通知模型,让其更新数据;
- 模型数据更新好之后,控制器会通知视图,告诉它模型的数据发生了变化;
- 视图接收到更新消息之后,会根据模型所提供的数据来生成新的虚拟 DOM;
- 新的虚拟 DOM 生成好之后,就需要与之前的虚拟 DOM 进行比较,找出变化的节点;
- 比较出变化的节点之后,React 将变化的虚拟节点应用到 DOM 上,这样就会触发 DOM 节点的更新;
- DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。
渐进式网页应用(PWA):它究竟解决了Web应用的哪些问题?#
PWA,全称是 Progressive Web App,翻译过来就是渐进式网页应用。根据字面意思,它就是“渐进式 +Web 应用”。它是一套理念,渐进式增强 Web 的优势,并通过技术手段渐进式缩短和本地应用或者小程序的距离。基于这套理念之下的技术都可以归类到 PWA。
Web 应用 VS 本地应用#
- Service Worker:解决离线存储和消息推送的问题
在页面和网络之间增加一个拦截器,用来缓存和拦截请求。在没有安装 Service Worker 之前,WebApp 都是直接通过网络模块来请求资源的。安装了 Service Worker 模块之后,WebApp 请求资源时,会先通过 Service Worker,让它判断是返回 Service Worker 缓存的资源还是重新去网络请求资源。一切的控制权都交由 Service Worker 来处理。 - manifest.json: 解决一级入口的问题
Service Worker 的设计思路#
- 架构
“让其运行在主线程之外”就是 Service Worker 来自 Web Worker 的一个核心思想。 Service Worker 需要在 Web Worker 的基础之上加上储存功能。由于 Service Worker 还需要会为多个页面提供服务,所以还不能把 Service Worker 和单个页面绑定起来。 - 消息推送
消息推送也是基于 Service Worker 来实现的。消息推送时,浏览器页面也许并没有启动,这时就需要 Service Worker 来接收服务器推送的消息,并将消息通过一定方式展示给用户。 - 安全
所以要使站点支持 Service Worker,首先必要的一步就是要将站点升级到 HTTPS。
WebComponent:像搭积木一样构建Web应用#
对内高内聚,对外低耦合。
阻碍前端组件化的因素#
CSS 的全局属性会阻碍组件化,DOM 也是阻碍组件化的一个因素,因为在页面中只有一个 DOM,任何地方都可以直接读取和修改 DOM。
WebComponent 组件化开发#
提供了对局部视图封装能力,可以让 DOM、CSSOM 和 JavaScript 运行在局部环境中,这样就使得局部的 CSS 和 DOM 不会影响到全局。WebComponent 是一套技术的组合,具体涉及到了Custom elements(自定义元素)、Shadow DOM(影子 DOM)和HTML templates(HTML 模板)
- template 属性来创建模板:
- DOM 树中的 template 节点不会出现在布局树中(不会渲染到页面上);
- 可以被重复使用
- 创建一个类
- 查找模板内容;
- 创建影子 DOM;把影子 DOM 看成是一个作用域,其内部的样式和元素是不会影响到全局的样式和元素,而在全局环境下,要访问影子 DOM 内部的样式或者元素也是需要通过约定好的接口;在影子 DOM 定义的 JavaScript 函数可以被外部访问,因为 JavaScript 语言本身已经可以很好地实现组件化。
- 再将模板添加到影子 DOM 上;
- 使用 customElements.define 来自定义元素。
- 像正常使用 HTML 元素一样使用该元素
<geek-bang></geek-bang>
浏览器如何实现影子 DOM#
HTTP/1:HTTP性能优化#
超文本传输协议 HTTP/0.9#
- 只有一个请求行,并没有HTTP 请求头和请求体。
- 服务器也没有返回头信息。
- 返回的文件内容是以 ASCII 字符流来传输。
被浏览器推动的 HTTP/1.0#
- 浏览器和服务器知道数据类型
- 压缩方式传输
- 语言版本的页面
- 文件的编码类型
- 状态码
- Cache 机制
- 统计客户端的基础信息用户代理字段
缝缝补补的 HTTP/1.1#
- 改进持久连接:特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。持久连接在 HTTP/1.1 中是默认开启,不想要采用持久连接,在 HTTP 请求头中加上
Connection: close
,浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。 - 不成熟的 HTTP 管线化
持久连接虽然能减少 TCP 的建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果 TCP 通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题。HTTP/1.1 中试图通过管线化的技术来解决队头阻塞的问题。HTTP/1.1 中的管线化是指将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。(目前没有实现) - 提供虚拟主机的支持
一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址。(Host 字段,用来表示当前的域名地址) - 对动态生成的内容提供了完美支持
浏览器在传输数据之前并不知道最终的数据大小,这就导致了浏览器不知道何时会接收完所有的文件数据。Chunk transfer 机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。 - 客户端 Cookie、安全机制
域名分片机制MDN
HTTP/2:如何提升网络速度?#
HTTP/1.1 的主要问题#
对带宽的利用率却并不理想,带宽是指每秒最大能发送或者接收的字节数。发送是上行带宽,接受是下行宽带。
- TCP 的慢启动。
一旦一个 TCP 连接建立之后,就进入了发送数据状态,刚开始 TCP 协议会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态,我们把这个过程称为慢启动。 - 同时开启了多条 TCP 连接,那么这些连接会竞争固定的带宽。
- HTTP/1.1 队头阻塞的问题。(持久连接一个管道中同一时刻只能处理一个请求)
HTTP/2 的多路复用#
总结为:继续使用TCP建立连接,一个域名只使用一个 TCP 长连接和消除队头阻塞问题。
每个请求都有一个id标识,这是http/2的request特点,根据id可以不用按照顺序发送request,浏览器和服务端可以通过id来进行组合大文件类型的请求(数据较大一次发送不完),服务端也可以通过id来优先决定先处理并返回关键资源的请求。
多路复用的实现#
通过引入二进制分帧层,实现了 HTTP 的多路复用技术。
HTTP/2 添加了一个二进制分帧层,那我们就结合图来分析下 HTTP/2 的请求和接收过程:
- 首先,浏览器准备好请求数据,包括了请求行、请求头等信息,如果是 POST 方法,那么还要有请求体。
- 这些数据经过二进制分帧层处理之后,会被转换为一个个带有请求 ID 编号的帧,通过协议栈将这些帧发送给服务器。
- 服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。
- 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
- 同样,二进制分帧层会将这些响应数据转换为一个个带有请求 ID 编号的帧,经过协议栈发送给浏览器。
- 浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求。
HTTP/2 其他特性#
- 可以设置请求的优先级
- 服务器推送:请求一个html文件可以相继的发送html里链接的CSS和JavaScript文件;(CSS和JavaScript的请求可能是浏览器发起的)
- 头部压缩
http/2还是存在队头阻塞的问题:TCP传输过程中把一份数据分为多个数据包的。当其中一个数据包没有按照顺序返回,接收端会一直保持连接等待数据包返回,这时候就会阻塞后续请求。
HTTP/1.1 为了提升并行下载效率,浏览器为每个域名维护了 6 个 TCP 连接;而采用 HTTP/2 之后,浏览器只需要为每个域名维护 1 个 TCP 持久连接,同时还解决了 HTTP/1.1 队头阻塞的问题。
HTTP/3:甩掉TCP、TLS 的包袱,构建高效网络#
TCP 的队头阻塞#
TCP 最初就是为了单连接而设计,TCP 连接看成是两台计算机之前的一个虚拟管道,计算机的一端将要传输的数据按照顺序放入管道,最终数据会以相同的顺序出现在管道的另外一头。
在 TCP 传输过程中,由于单个数据包的丢失而造成的阻塞称为 TCP 上的队头阻塞。(http/2队头阻塞是发生在一个tcp连接管道,同一个域名中)
HTTP/2 多个请求是跑在一个 TCP 管道中的,其中任意一路数据流出现了丢包的情况,就会阻塞该 TCP 连接中的所有请求。这不同于 HTTP/1.1,使用 HTTP/1.1 时,浏览器为每个域名开启了 6 个 TCP 连接,如果其中的 1 个 TCP 连接发生了队头阻塞,那么其他的 5 个连接依然可以继续传输数据。
所以随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。
TCP 建立连接的延时#
TCP 的握手过程也是影响传输效率的一个重要因素。把从浏览器发送一个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间称为 RTT,RTT 是反映网络性能的一个重要指标。
以https协议为例,计算延迟过程:
- 建立 TCP 连接三次握手来确认连接成功,需要在消耗完 1.5 个 RTT 之后才能进行数据传输。
- 进行 TLS 连接,需要进行第二次握手,TLS 有两个版本——TLS1.2 和 TLS1.3,每个版本建立连接所花的时间不同,大致是需要 1~2 个 RTT。
TCP 协议僵化#
TCP 协议存在队头阻塞和建立连接延迟等缺点可以改进吗?非常困难
- 中间设备的僵化:这些设备包括了路由器、防火墙、NAT、交换机等,些软件使用了大量的 TCP 特性,这些功能被设置之后就很少更新。
- 操作系统:TCP 协议都是通过操作系统内核来实现的,应用程序只能使用不能修改。
开发新的协议,也会出现TCP 协议僵化相同的困境,没有设备支持
QUIC 协议#
HTTP/3 选择了一个折衷的方法——UDP 协议,基于 UDP 实现了类似于 TCP 的多路数据流、传输可靠性等功能,我们把这套功能称为QUIC 协议。
HTTP/3 中的 QUIC 协议集合了以下几点功能:
- 实现了类似 TCP 的流量控制、传输可靠性的功能。虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。
- 集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。
- 实现了 HTTP/2 中的多路复用功能。和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。
- 实现了快速握手功能。可以实现使用 0-RTT 或者 1-RTT 来建立连接。
HTTP/3 的挑战#
- 服务器和浏览器端都没有对 HTTP/3 提供比较完整的支持;
- 部署 HTTP/3,UDP 的优化远远没有达到 TCP 的优化程度;
- 中间设备僵化的问题,中间设备 UDP 的优化程度远远低于 TCP ,据统计使用 QUIC 协议时,大约有 3%~7% 的丢包率。
同源策略:为什么XMLHttpRequest不能跨域请求资源?#
浏览器安全可以分为三大块——Web 页面安全、浏览器网络安全和浏览器系统安全。
什么是同源策略#
如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。浏览器默认两个相同的源之间是可以相互访问资源和操作 DOM 的。
具体来讲,同源策略主要表现在 DOM、Web 数据和网络这三个层面。
- DOM 层面。同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。
- 数据层面。同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。
- 网络层面。同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点
安全和便利性的权衡#
浏览器出让了同源策略的安全性
- 页面中可以嵌入第三方资源:任意引用外部文件
但此行为也衍生出了XSS攻击,在html中恶意植入JavaScript文件,恶意脚本读取 Cookie 数据,并将其作为参数添加至恶意站点尾部,当用户不小心打开该恶意页面时,恶意服务器就能接收到当前用户的 Cookie 信息。
浏览器中引入了内容安全策略,称为 CSP。CSP 的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联 JavaScript 代码。 - 跨域资源共享和跨文档消息机制
- 跨域资源共享(CORS)
- 跨文档消息机制,通过 window.postMessage 的 JavaScript 接口来和不同源的 DOM 进行通信
跨站脚本攻击(XSS):为什么Cookie中有HttpOnly属性?#
什么是 XSS 攻击#
XSS 全称是 Cross Site Scripting,为了与“CSS”区分开来,故简称 XSS,翻译过来就是“跨站脚本”。XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
- 窃取 Cookie 信息
- 监听用户行为
- 修改 DOM
- 在页面内生成浮窗广告
恶意脚本是怎么注入的#
存储型 XSS 攻击、反射型 XSS 攻击和基于 DOM 的 XSS 攻击三种方式来注入恶意脚本。
- 存储型 XSS 攻击
- 首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中;
- 然后用户向网站请求包含了恶意 JavaScript 脚本的页面;
- 当用户浏览该页面的时候,恶意脚本就会将用户的 Cookie 信息等数据上传到服务器。
- 反射型 XSS 攻击
在一个反射型 XSS 攻击过程中,恶意 JavaScript 脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。 - 基于 DOM 的 XSS 攻击
基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的,在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
如何阻止 XSS 攻击#
存储型 XSS 攻击和反射型 XSS 攻击是服务器漏洞,DOM 的 XSS 攻击是前端漏洞。
- 服务器对输入脚本进行过滤或转码
- 充分利用 CSP
- 限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是无法被加载的;
- 禁止向第三方域提交数据,这样用户数据也不会外泄;
- 禁止执行内联脚本和未授权的脚本;
- 还提供了上报机制,这样可以帮助我们尽快发现有哪些 XSS 攻击,以便尽快修复问题。
- 使用 HttpOnly 属性
set-cookie:id=1;HttpOnly
,set-cookie 属性值最后使用了 HttpOnly 来标记该 Cookie,无法通过 JavaScript 来读取这段 Cookie。
CSRF攻击:陌生链接不要随便点#
什么是 CSRF 攻击#
CSRF 英文全称是 Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。
- 自动发起 Get 请求
<img src="https://time.geekbang.org/sendcoin?user=hacker&number=100">
- 自动发起 POST 请求
<h1> 黑客的站点:CSRF 攻击演示 </h1>
<form id='hacker-form' action="https://time.geekbang.org/sendcoin" method=POST>
<input type="hidden" name="user" value="hacker" />
<input type="hidden" name="number" value="100" />
</form>
<script> document.getElementById('hacker-form').submit(); </script>
- 引诱用户点击链接
<a href="https://time.geekbang.org/sendcoin?user=hacker&number=100" taget="_blank">
点击下载美女照片
</a>
和 XSS 不同的是,CSRF 攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来实施攻击。
如何防止 CSRF 攻击#
发起 CSRF 攻击的三个必要条件:
- 目标站点一定要有 CSRF 漏洞;
- 用户要登录过目标站点,并且在浏览器上保持有该站点的登录状态;
- 需要用户打开一个第三方站点,可以是黑客的站点,也可以是一些论坛。
要让服务器避免遭受到 CSRF 攻击,通常有以下几种途径:
- 充分利用好 Cookie 的 SameSite 属性
- Strict 最为严格。如果 SameSite 的值是 Strict,那么浏览器会完全禁止第三方 Cookie。简言之,如果你从极客时间的页面中访问 InfoQ 的资源,而 InfoQ 的某些 Cookie 设置了 SameSite = Strict 的话,那么这些 Cookie 是不会被发送到 InfoQ 的服务器上的。只有你从 InfoQ 的站点去请求 InfoQ 的资源时,才会带上这些 Cookie。
- Lax 相对宽松一点。在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交 Get 方式的表单这两种方式都会携带 Cookie。但如果在第三方站点中使用 Post 方法,或者通过 img、iframe 等标签加载的 URL,这些场景都不会携带 Cookie。
- 而如果使用 None 的话,在任何情况下都会发送 Cookie 数据。
- 验证请求的来源站点
在服务器端验证请求来源的站点,Referer 字段,记录了该 HTTP 请求的来源地址,该字段不是必须的。Origin字段,如XMLHttpRequest、Fecth 发起跨站请求或者通过 Post 方法发送请求时,都会带上 Origin 属性。优先判断 Origin,再根据实际情况判断 Referer 值。 - CSRF Token
- 第一步,在浏览器向服务器发起请求时,服务器生成一个 CSRF Token。
- 第二步,在浏览器端如果要发起转账的请求,那么需要带上页面中的 CSRF Token,然后服务器会验证该 Token 是否合法。
安全沙箱:页面和系统之间的隔离墙#
安全视角下的多进程架构#
安全沙箱#
将渲染进程和操作系统隔离的这道墙就是我们要聊的安全沙箱。浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程。
安全沙箱如何影响各个模块功能#
安全沙箱最小的保护单位是进程,并且能限制进程对操作系统资源的访问和修改,这就意味着如果要让安全沙箱应用在某个进程上,那么这个进程必须没有读写操作系统的功能,比如读写本地文件、发起网络请求、调用 GPU 接口等。
渲染进程和浏览器内核各自都有哪些职责,如下图:
- 持久存储
- 存储 Cookie 数据的读写。通常浏览器内核会维护一个存放所有 Cookie 的 Cookie 数据库,然后当渲染进程通过 JavaScript 来读取 Cookie 时,渲染进程会通过 IPC 将读取 Cookie 的信息发送给浏览器内核,浏览器内核读取 Cookie 之后再将内容返回给渲染进程。
- 一些缓存文件的读写也是由浏览器内核实现的,比如网络文件缓存的读取。
- 网络访问
- 用户交互
站点隔离(Site Isolation)#
所谓站点隔离是指 Chrome 将同一站点(包含了相同根域名和相同协议的地址)中相互关联的页面放到同一个渲染进程中执行。包括在标签也中使用 iframe ,站点隔离会将不同源的 iframe 分配到不同的渲染进程中(而不是以标签页区分的渲染进程,这样iframe恶意第三方地址不会和正常站点公用一个渲染进程)。
HTTPS:让数据传输更安全#
在 HTTP 协议栈中引入安全层#
HTTPS 安全层有两个主要的职责:对发起 HTTP 请求的数据进行加密操作和对接收到 HTTP 的内容进行解密操作。
第一版:使用对称加密#
- 浏览器发送它所支持的加密套件列表和一个随机数 client-random,这里的加密套件是指加密的方法,加密套件列表就是指浏览器能支持多少种加密方法列表。
- 服务器会从加密套件列表中选取一个加密套件,然后还会生成一个随机数 service-random,并将 service-random 和加密套件列表返回给浏览器。
- 最后浏览器和服务器分别返回确认消息。
问题:
传输 client-random 和 service-random 的过程是明文的,黑客可以拿到协商的加密套件和双方的随机数,随机数合成密钥的算法是公开的,所以黑客拿到随机数之后,也可以合成密钥。
第二版:使用非对称加密#
非对称加密算法有 A、B 两把密钥,如果你用 A 密钥来加密,那么只能使用 B 密钥来解密;反过来,如果你要 B 密钥来加密,那么只能用 A 密钥来解密。
公钥在浏览器端,私钥在服务器端,浏览器发送给服务端的数据需要用私钥解密,服务端发给浏览器的数据需要公钥解密,但在请求的开始,公钥和数据会一同发给客户端。
问题:
- 第一个是非对称加密的效率太低。
- 第二个是无法保证服务器发送给浏览器的数据安全。(服务器的数据可以通过公钥解密,但公钥是公开的)
第三版:对称加密和非对称加密搭配使用#
在传输数据阶段依然使用对称加密,但是对称加密的密钥我们采用非对称加密来传输。
pre-master 是经过公钥加密之后传输的,所以黑客无法获取到 pre-master,这样黑客就无法生成密钥,也就保证了黑客无法破解传输过程中的数据了。
加密套件是指加密的方法,加密套件列表就是指浏览器能支持多少种加密方法列表,提供给服务器选择。
第四版:添加数字证书#
服务器要证明这个服务器就是自己,需要使用权威机构颁发的证书,这个权威机构称为CA(Certificate Authority),颁发的证书就称为数字证书(Digital Certificate)。
数字证书有两个作用:一个是通过数字证书向浏览器证明服务器的身份,另一个是数字证书里面包含了服务器公钥。
公钥包含在数字证书中,并且需要验证数字证书是哪个服务器
申请免费证书
中文:https://freessl.cn/
英文:https://www.freessl.com/
如何申请数字证书#
- 准备一套私钥和公钥
- 向 CA 机构提交公钥、公司、站点等信息并等待认证
- 审核通过,CA 会向极客时间签发认证的数字证书,包含了公钥、组织信息、CA 的信息、有效时间、证书序列号等,这些信息都是明文的,同时包含一个 CA 生成的签名。
数字签名的过程: CA 使用 Hash 函数来计算极客时间提交的明文信息,并得出信息摘要;然后 CA 再使用它的私钥对信息摘要进行加密,加密后的密文就是 CA 颁给极客时间的数字签名。
浏览器如何验证数字证书#
浏览器读取证书中相关的明文信息,采用 CA 签名时相同的 Hash 函数来计算并得到信息摘要 A;再利用对应 CA 的公钥解密签名数据,得到信息摘要 B;如果信息摘要 A 和信息摘要 B 一致,确认证书是合法的。
浏览上下文组:如何计算Chrome中渲染进程的个数?#
标签页之间的连接#
通过<a>
标签来和新标签建立连接,通过 JavaScript 中的 window.open 方法来和新标签页建立连接,不论这两个标签页是否属于同一站点,他们之间都能通过 opener 来建立连接,所以他们之间是有联系的,在 WhatWG 规范中,把这一类具有相互连接关系的标签页称为浏览上下文组 ( browsing context group)。Chrome 浏览器会将浏览上下文组中属于同一站点的标签分配到同一个渲染进程中。
可以使用
<a>
标签中的 noopener 和 noreferrer 属性,来控制新打开的标签页是否需要浏览上下文组。
如果有 iframe 标签,并且地址不是同一站点,则会分配到不同渲染进程中
任务调度:有了setTimeOut,为什么还要使用rAF(requestAnimationFrame)?#
单消息队列的队头阻塞问题#
在单消息队列架构下,存在着低优先级任务会阻塞高优先级任务的情况
Chromium 是如何解决队头阻塞问题的?#
- 第一次迭代:引入高优先级队列
实现了三个不同优先级的消息队列,然后可以使用任务调度器来统一调度这三个不同消息队列中的任务,可以实现优先队列任务优先执行。
问题:将用户输入的消息或者合成消息添加进多个不同优先级的队列中,任务的相对执行顺序就会被打乱,甚至有可能出现还未处理输入事件,就合成了该事件要显示的图片。 - 第二次迭代:根据消息类型来实现消息队列
可以为不同类型的任务创建不同优先级的消息队列:- 输入事件的消息队列,用来存放输入事件。
- 合成任务的消息队列,用来存放合成事件。
- 默认消息队列,用来保存如资源加载的事件和定时器回调等事件。
- 创建一个空闲消息队列,用来存放 V8 的垃圾自动垃圾回收这一类实时性不高的事件。
问题:在页面加载阶段,如果依然要优先执行用户输入事件和合成事件,那么页面的解析速度将会被拖慢。
3. 第三次迭代:动态调度策略
- 页面加载阶段:诉求是在最短的时间看到页面,页面解析,JavaScript 脚本执行等任务调整为优先级最高的队列。
- 交互阶段:
前缓冲区存放着显示器要显示的图像(60HZ,1/60 秒读取一次前缓冲区),浏览器会将新生成的图片提交到显卡的后缓冲区中,GPU 会将后缓冲区和前缓冲区互换位置,也就完成显示器的图像显示。
当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号(vertical synchronization)给 GPU,简称 VSync。
当在执行用户交互的任务时,将合成任务的优先级调整到最高。主线程处理完成 DOM,计算好布局和绘制,需要将信息提交给合成线程来合成最终图片,主线程合成任务的优先级调整为最低,并将页面解析、定时器等任务优先级提升。
如果当前合成操作执行的非常快,比如用时8毫秒, VSync 同步周期是 16.66(1/60)毫秒,那么合成结束到下个 VSync 周期内,就进入了一个空闲时间阶段,那么就可以在这段空闲时间内执行一些不那么紧急的任务。
- 第四次迭代:任务饿死
在某个状态下,一直有新的高优先级的任务加入到队列中,就会导致其他低优先级的任务得不到执行。Chromium 给每个队列设置了执行权重,如果连续执行了一定个数的高优先级的任务,那么中间会执行一次低优先级的任务,可以缓解这种情况。
加载阶段性能:使用Audits来优化Web性能#
性能检测工具:Performance vs Audits#
Performance 和 Audits,能够准确统计页面在加载阶段和运行阶段的一些核心数据,诸如任务执行记录、首屏展示花费的时长等。Perfomance 能让我们看到更多细节数据,但是更加复杂,Audits 就比较智能,但是隐藏了更多细节。
利用 Audits 生成 Web 性能报告#
解读性能报告#
根据性能报告优化 Web 性能#
- 首次绘制 (First Paint):如果 FP 时间过久,那么直接说明了一个问题,那就是页面的 HTML 文件可能由于网络原因导致加载时间过久
- 首次有效绘制 (First Meaningfull Paint):由于 FMP 计算复杂,所以现在不建议使用该指标了,另外由于 LCP 的计算规则简单,所以推荐使用 LCP 指标,具体文章你可以参考这里。不过是 FMP 还是 LCP,优化它们的方式都是类似的,你可以结合上图,如果 FMP 和 LCP 消耗时间过久,那么有可能是加载关键资源花的时间过久,也有可能是 JavaScript 执行过程中所花的时间过久,所以我们可以针对具体的情况来具体分析。
- 首屏时间 (Speed Index):这就是我们上面提到的 LCP,它表示填满首屏页面所消耗的时间,首屏时间的值越大,那么加载速度越慢,具体的优化方式同优化第二项 FMP 是一样。
- 首次 CPU 空闲时间 (First CPU Idle):也称为 First Interactive,它表示页面达到最小化可交互的时间,也就是说并不需要等到页面上的所有元素都可交互,只要可以对大部分用户输入做出响应即可。要缩短首次 CPU 空闲时长,我们就需要尽可能快地加载完关键资源,尽可能快地渲染出来首屏内容,因此优化方式和第二项 FMP 和第三项 LCP 是一样的。
- 完全可交互时间 (Time to Interactive):简称 TTI,它表示页面中所有元素都达到了可交互的时长。简单理解就这时候页面的内容已经完全显示出来了,所有的 JavaScript 事件已经注册完成,页面能够对用户的交互做出快速响应,通常满足响应速度在 50 毫秒以内。如果要解决 TTI 时间过久的问题,我们可以推迟执行一些和生成页面无关的 JavaScript 工作。
- 最大估计输入延时 (Max Potential First Input Delay):这个指标是估计你的 Web 页面在加载最繁忙的阶段, 窗口中响应用户输入所需的时间,为了改善该指标,我们可以使用 WebWorker 来执行一些计算,从而释放主线程。另一个有用的措施是重构 CSS 选择器,以确保它们执行较少的计算。。
最后#
Chromium源码: https://chromium.googlesource.com/chromium/src
Chromium源码文档:https://chromium.googlesource.com/chromium/src/+/master/docs/README.md
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!