摘要:极客时间《浏览器工作原理与实践》 浏览器中的页面学习笔记,Chrome 开发者工具、DOM 树、渲染流水线、分层和合成机制、页面性能、虚拟 DOM、渐进式网页应用(PWA)、WebComponent
Chrome 开发者工具
面板介绍
更详细的可以查看《你不知道的 Chrome 调试技巧》笔记
网络面板
网络面板由控制器、过滤器、抓图信息、时间线、详细列表和下载信息概要这 6 部分组成:
1、控制器
2、过滤器
其主要就是起过滤功能,可以在众细列表中众多信息中筛选想要查看的文件类型
3、抓图信息
抓图信息区域可以用来分析用户等待页面加载时间内所看到的内容,分析用户实际的体验情况。
4、时间线
其主要是用来展示 HTTP、HTTPS、WebSocket 加载状态和时间的关系,如果是多条竖线堆叠在一起,那说明这些资源被同时被加载。
5、详细列表
其详细记录了每个资源从发起请求到完成请求这中间所有过程的状态,以及最终请求完成的数据信息,是我们关注的重点区域。
6、下载信息概要
重点关注下 DOMContentLoaded 和 Load 两个事件及其触发时间:
1)DOMContentLoaded
这个事件发生后,说明页面已经构建好 DOM 了,这意味着构建 DOM 所需要的 HTML 文件、JavaScript 文件、CSS 文件都已经下载完成。
2)Load
这个事件发生后,说明浏览器已经加载了所有的资源(图像、样式表等)
详细列表
1、列表的属性
列表的属性很多,还可以通过在其上右键添加其他属性,还可以按照列表的属性来给列表排序。列表默认是按请求发起的时间来排序的,最早发起请求的资源在顶部,点击相应属性即可进行排序。
2、详细信息
选中详细列表中的一项,右边会展示该项的详细信息:
3、单个资源的时间线
查看详细信息中的时间线:
其中各个阶段:
1)Queuing,排队。发起请求时不能立刻执行,排队等待请求。排队的原因:
①优先级高的资源优先加载,比如 CSS、HTML、JavaScript。优先级低的资源要让路排队,比如图片、音频、视频等。
②每个域名最多 6 个 TCP 连接,超出的请求就要排队等待。
③网络进程在为数据分配磁盘空间时,新的 HTTP 请求也需要短暂地等待磁盘分配结束。
2)Stalled,停滞。有一些原因可能导致发起连接的过程被推迟
3)Proxy Negotiation,代理协商阶段。它表示代理服务器连接协商所用的时间,如果没有使用代理服务器就不会有这个阶段。
4)Initial connection,建立连接阶段。它包括了建立 TCP 连接所花费的时间。
5)SSL,SSL 握手阶段。主要是 SSL 握手的时间,使用 HTTPS 协议时会有这个阶段。
6)Request sent,准备并发送请求数据阶段。这个时建很短,只需要将浏览器缓冲区的数据发送出去即可,无需判断服务器是否接收到数据。
7)Waiting (TTFB),第一字节时间。从数据发送出去到接收服务器第一个字节数据等待的时间。TTFB 时间越短,就说明服务器响应越快。
8)Content Download,接收全部响应数据用时。
各项优化
1、排队(Queuing)时间过久
解决方法:
①域名分片技术,一个域名最多 6 个 TCP 连接,多个域名进行请求则可以创建更多的 TCP 连接
②使用 HTTP2,直接取消最多 6 个 TCP 连接的限制
2、第一字节时间(TTFB)时间过久
可能的原因:
①服务器生成页面数据的时间过久,可能在拼装数据时发生问题,也可能是数据量过大造成的。
②网络的原因,网太慢。
③发送请求头时带上了多余的用户信息,导致服务器处理用时太久。
解决方法:
①更换硬件设备,提高服务器运行速度,或者使用各种缓存技术。
②更新网络设备,使用 CDN 来缓存一些静态文件。
③精简请求数据,减少 Cookie 数据信息。
3、Content Download 时间过久
有可能是相应数据的字节数过大导致的,采用删除注释、压缩文件等方法减少文件大小。
DOM 树
什么是 DOM
DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容
DOM 树如何生成
网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型。如果是一个 HTML 类型的文件,浏览器就会为该请求选择或者创建一个渲染进程。渲染进程创建好后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程加载数据不断的放入管道中,另一头的 HTML 解析器不停的解析数据。字节流转换为 DOM 需要三个阶段:
1、通过分词器将字节流转换为 Token。Token 分为 Tag Token 和文本 Token,Tag Token 又分 StartTag 和 EndTag
2 和 3 是同步进行的,将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中
HTML 解析器维护了一个 Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构,同时会将一个 StartTag document 的 Token 压入栈底。在第 1 步中生成的 Token 会被按照顺序压到这个栈中。具体处理规则:
如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。比如经过分词器解析出来的第一个 StartTag html Token 会被压入到栈中,并创建一个 html 的 DOM 节点,添加到 document 上。如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。通过分词器产生的新 Token 就这样不停地压栈和出栈,直到分词器将所有字节流分词完成。
JAVASCRIPT 影响 DOM 解析
解析 DOM 时,如果遇到 JavaScript 标签,会暂停整个 DOM 的解析,先下载 JavaScript 文件,即 JavaScript 文件的下载过程会阻塞 DOM 解析。Chrome 自身做了一些优化,比如预解析操作。当渲染引擎收到字节流后会开启一个预解析线程,解析到相关的 JavaScript、CSS 文件后,预解析线程会提前下载这些文件。可以使用 CDN 来加速加载 JavaScript 文件,或者压缩 JavaScript 文件体积。如果 JavaScript 文件中没有操作 DOM 的代码,可以添加 async 或 defer 标识来异步加载。
async:文件一加载完就立即执行,可能在 DOMContentLoaded 之前,也可能在之后
defer:加载完后并不是立即执行,而是在 DOMContentLoaded 之前执行
如果代码里引用了外部的 CSS 文件,那么渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操作了 DOM,都会执行 CSS 文件下载,解析 CSS,再执行 JavaScript 脚本。也就是说 JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞 JavaScript 的执行。而且,如果执行 JavaScript 操作 DOM 的代码时,如果还没有解析对应的 DOM,那么代码不会起作用。
渲染流水线中的 CSS
渲染引擎无法直接理解 CSS 文件内容,需要将 CSS 解析成渲染引擎能够理解的结构 CSSOM,它体现在 DOM 中就是 document.styleSheets,它具有两个作用:
1、提供给 JavaScript 操作样式表的能力;
2、为布局树的合成提供基础的样式信息。
有了 DOM 和 CSSOM 就可以构建布局树,布局树的结构基本上就是复制 DOM 树的结构,但是 DOM 树中不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。之后就是通过样式计算和计算布局完成布局树的构建。
如果 HTML 文件中包含了 CSS 的外部引用和 JavaScript 外部文件,那么其渲染流水线:
优化策略
从发起 URL 请求开始,到首次显示页面的内容,在视觉上经历的三个阶段:
1、请求发出去到提交数据阶段,这时页面展示出来的还是之前页面的内容;
2、提交数据之后渲染进程会创建一个空白页面,等待 CSS 文件和 JavaScript 文件的加载完成,生成 DOM 和 CSSOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染,这段时间称为解析白屏;
3、等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。
重点关注第二点白屏时间,通常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript。
优化策略:
1、内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程
2、尽量减少文件大小,消除注释和不用的代码,压缩 JavaScript 文件
3、对不会修改 DOM 的 JavaScript 文件添加 async 或者 defer 属性
4、通过媒体查询属性将大的 CSS 文件拆分为多个不同用途的 CSS 文件
分层和合成机制
显示图像
显卡的职责是合成新的图像,并将图像保存到后缓冲区中。之后系统就会让后缓冲区和前缓冲区互换,然后显示器就将显卡中前缓冲区的图像显示到屏幕上。每个显示器都有固定的刷新频率,通常是 60Hz,即每秒更新 60 张图片,显卡的更新频率和显示器的刷新频率一般是一致的,但在一些复杂的场景中,显卡处理的速度会变慢,这样就会造成页面的卡顿。渲染流水线上生成的每一副图片被称为一帧,渲染流水线每秒更新了多少帧称为帧率。如果 1 秒更新 60 帧,那么帧率就是 60Hz。重排、重绘和合成这三种方式都可以生成一帧,但三种方式的渲染路径是不同的,通常渲染路径越长,生成图像花费的时间就越多。
分层、分块和合成
Chrome 中的合成技术,可以用三个词来概括总结:分层、分块和合成。
1、分层和合成过程
分层和合成通常是一起使用的。在 Chrome 的渲染流水线中,分层体现在生成布局树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree)。创建层树后,渲染引擎会依据层树的结点生成绘制指令列表。然后进行光栅化,按照绘制列表中的指令,将每个图层都绘制成一张图片。合成线程会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。
2、分块
合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块。并且在首次合成图块的时候使用一个低分辨率的图片,这是因为从计算机内存到 GPU 内存进行纹理上传的操作比较慢。分辨率减少一半,纹理就减少了四分之三。先显示低分辨率的图片,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。
页面性能
通常一个页面有三个阶段:
1、加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。
2、交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
3、关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。
重点是加载阶段和交互阶段
加载阶段
JavaScript 脚本、首次请求的 HTML 资源文件、CSS 文件会阻塞首次渲染,这些能阻塞网页首次渲染的资源称为关键资源。关键资源影响页面的核心因素:
1、关键资源个数,关键资源个数越多,首次页面的加载时间越长。
2、关键资源大小,通常情况下,所有关键资源的内容越小,下载越快,阻塞渲染的时间越短。
3、请求关键资源需要多少个 RTT(Round Trip Time),RTT 是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。通常 1 个 HTTP 的数据包在 14KB 左右。
由于渲染引擎有一个预解析的线程,在接收到 HTML 数据之后,预解析线程会快速扫描 HTML 数据中的关键资源,可以认为 JavaScript 和 CSS 是同时发起请求的,所以他们的请求时重叠的,只需要计算 JavaScript 脚本和 CSS 文件中体积最大的那个文件的 RTT 即可。
知道了影响页面渲染的核心因素,总的优化原则也就明确:
1、减少关键资源个数,将 JavaScript 和 CSS 资源改成内联模式,如果 JavaScript 脚本不操作 DOM,可以添加 async 或 defer 属性,CSSlink 可以加上取消阻止显现的标志 media 属性
2、减小关键资源大小,压缩、去除注释或无用的代码
3、降低关键资源的 RTT 次数,通过前两项减少关键资源的个数和大小降低次数,还可以使用 CDN 来减少每次 RTT 时长
交互阶段
在交互阶段优化的原则是让单个帧的生成速度变快。最好是合成,然后是重绘,最差是重排。
加快单个帧生成速度方法:
1、减少 JavaScript 脚本执行时间
脚本函数执行时间过长会严重影响主线程执行其他渲染任务的时间,优化方法:
1)将一次执行的函数分解为多个任务,使每次执行的时间减短
2)采用 Web Workers,但 Web Workers 仅能执行 JavaScript 脚本,不能访问 DOM
2、避免强制同步布局
所谓强制同步布局,是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中。一般情况下,通过 DOM 接口执行添加或删除元素等操作后,会在另外的任务中异步执行重新计算样式和布局的任务。但是如果在当前任务中直接查询更新后 DOM 的相关信息,那么就会触发强制同步布局,在当前任务中强制执行一次布局操作。
3、避免布局抖动
布局抖动是指在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。尽量不要在修改 DOM 结构时再去查询一些相关值
4、合理利用 CSS 合成动画
即使主线程被 JavaScript 或者一些布局任务占用,CSS 动画也依然能继续执行,因为合成操作由合成线程完成
5、避免频繁的垃圾回收
垃圾回收器频繁地去执行垃圾回收策略,会长时间占用主线程,从而影响到其他任务的执行。因此要尽量避免产生那些临时垃圾数据,尽可能优化储存结构,尽可能避免小颗粒对象的产生。
虚拟 DOM
每次操作 DOM 都会消耗不少资源,如果还有引发强制同步布局和布局抖动的不当操作,会大大降低渲染效率。
虚拟 DOM 解决的问题:
1、将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上
2、变化应用到虚拟 DOM 上时,虚拟 DOM 并不是立刻去渲染页面,而仅仅是调整虚拟 DOM 的内部状态
3、在虚拟 DOM 搜集到足够的改变后,将这些变化一次性应用到真是的 DOM 上
双缓存
使用双缓存,先将计算的中间结果存放在另一个缓冲区中,等全部的计算结束,该缓冲区已经存储了完整的图形之后,再将该缓冲区的图形数据一次性复制到显示缓冲区。即使像图形显示这样有很复杂且需要大量运算的情况,整个图像的输出也会非常稳定。可以把虚拟 DOM 看成是 DOM 的一个 buffer,和图形显示一样,它会在完成一次完整的操作之后,再把结果应用到 DOM 上,这样就能减少一些不必要的更新,同时还能保证 DOM 的稳定输出。
MVC 模式
MVC 的整体结构比较简单,由模型、视图和控制器组成,其核心思想就是将数据和视图分离,也就是说视图和模型之间是不允许直接通信的,它们之间的通信都是通过控制器来完成的。
可以把 React/Vue 的部分看成是一个 MVC 中的视图:
具体实现过程如下:
1、控制器会监控 DOM 的变化,一旦 DOM 发生变化,控制器变会通知模型进行更新数据;
2、模型更新好之后,控制器会通知视图模型已经更新;
3、视图收到更新消息后,会根据模型提供的数据来生成新的虚拟 DOM;
4、新的虚拟 DOM 生成好之后,会与老的虚拟 DOM 进行比较并找出变化的节点;
5、React/Vue 框架将变化的虚拟节点应用到 DOM 上,触发 DOM 节点的更新;
6、DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。
渐进式网页应用(PWA)
PWA 是一套理念,渐进式增强 Web 的优势,并通过技术手段渐进式缩短和本地应用或者小程序的距离。基于这套理念之下的技术都可以归类到 PWA。
WEB 应用 VS 本地应用
相对于本地应用,Web 应用的缺陷:
1、缺少离线使用功能
2、缺少消息推送功能
3、缺少一级入口
PWA 提出了两种解决方案:通过引入 Service Worker 来试着解决离线存储和消息推送的问题,通过引入 manifest.json 来解决一级入口的问题。
SERVICE WORKER
Service Worker 的主要思想是在页面和网络之间增加一个拦截器,用来缓存和拦截请求。当 WebApp 请求资源时,会先通过 Service Worker 判断是返回 Service Worker 缓存的资源还是重新去网络请求资源。
Service Worker 的设计思路:
1、架构
Service Worker 的核心思想是让其运行在主线程之外,需要在 Web Worker 的基础之上加上储存功能。在目前的 Chrome 架构中,Service Worker 是运行在浏览器进程中的,在浏览器的生命周期内,要为所有的页面提供服务。
2、消息推送
消息推送也是基于 Service Worker 来实现的,Service Worker 来接收服务器推送的消息,并将消息通过一定方式展示给用户。
3、安全
HTTP 采用的是明文传输信息,存在被窃听、被篡改和被劫持的风险,因而 Service Worker 采用 HTTPS 协议。此外,Service Worker 还需要同时支持 Web 页面默认的安全策略。
WebComponent
Web Components 由 Custom elements(自定义元素)、Shadow DOM(影子 DOM)和 HTML templates(HTML 模板)三项技术组成。
组件化
对内高内聚,对外低耦合即是组件化。
CSS 和 DOM 是阻碍组件化的两个因素:
1、CSS 的全局属性会相互影响
2、DOM 可以在任何地方被直接读取和修改
如何使用
三个步骤来使用 WebComponent:
1、使用 template 标签来创建模板,DOM 树中的 template 节点不会出现在布局树中。模板定义好之后,还需要在模板的内部定义样式信息。
2、创建一个类,在其构造函数中要完成 3 件事:
1)查找模板内容
2)创建影子 DOM
3)将模板添加到影子 DOM 上
影子 DOM 的作用是将模板中的内容与全局 DOM 和 CSS 进行隔离。在全局环境下,需要通过约定好的接口来访问影子 DOM 内部的样式或者元素。影子 DOM 的类封装好后,就可以使用 customElements.define 来自定义元素。
3、像正常使用 HTML 元素一样使用该元素
通过影子 DOM 可以隔离 CSS 和 DOM,但 JavaScript 脚本是不会被隔离的,在影子 DOM 定义的 JavaScript 函数依然可以被外部访问。
1 | <!DOCTYPE html> |
影子 DOM
WebComponent 的核心就是影子 DOM。使用几次自定义标签就会创建几个影子 DOM,并且每个影子 DOM 都有一个 shadow root 的根节点。每个影子 DOM 都可以看成是一个独立的 DOM,它有自己的样式、自己的属性,无法通过 DOM API 直接查询到影子 DOM 的内部元素。且影子 DOM 内部样式不会影响到外部样式,外部样式也不会影响到内部样式,影子 DOM 内部元素的节点选择 CSS 样式时,会直接使用影子 DOM 内部的 CSS 属性。