渲染流水线
渲染流水线视角下的CSS
下图为含有CSS的页面渲染流水线:
首先是发起主页面的请求,这个发起请求方可能是渲染进程,也有可能是浏览器进程,发起的请求被送到网络进程中去执行。网络进程接收到返回的 HTML 数据之后,将其发送给渲染进程,渲染进程会解析 HTML 数据并构建 DOM。这里你需要特别注意下,请求 HTML 数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈。
渲染流水线需要CSSOM
和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是 CSSOM。和 DOM 一样,CSSOM 也具有两个作用,第一个是提供给 JavaScript 操作样式表的能力,第二个是为布局树的合成提供基础的样式信息。 这个 CSSOM 体现在 DOM 中就是document.styleSheets。
影响页面展示的因素以及优化策略
渲染流水线影响到了首次页面展示的速度,而首次页面展示的速度又直接影响到了用户体验
那么接下来我们就来看看从发起 URL 请求开始,到首次显示页面的内容,在视觉上经历的三个阶段。
- 第一个阶段,等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。
- 第二个阶段,提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
- 第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。
通常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript。所以要想缩短白屏时长,可以有以下策略:
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 async 或者 defer。
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
通过以上策略就能缩短白屏展示的时长了,不过在实际项目中,总是存在各种各样的情况,这些策略并不能随心所欲地去引用,所以还需要结合实际情况来调整最佳方案。
分层和合成机制
显示器显示图像的原理:
每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒更新 60 张图片,更新的图片都来自于显卡中一个叫前缓冲区的地方,显示器所做的任务很简单,每秒固定读取60次前缓冲区的图像,并将读取的图像显示到显示器上。
帧和帧率的概念:
把渲染流水线生成的每一副图片称为一帧,把渲染流水线每秒更新了多少帧称为帧率。 比如滚动过程中 1 秒更新了 60 帧,那么帧率就是 60Hz(或者 60FPS)。
渲染引擎是如何实现一帧图像的?
渲染引擎生成一帧图像有三种方式:重绘、重排和合成
-
重绘和重排都是在渲染进程的主线程上执行的,比较耗时;
-
合成操作是在渲染进程的合成线程上执行的,执行速度快,且不占用主线程。
浏览器是怎么实现合成的?
- 分层:将素材分解成多个图层的操作,从宏观上提升了渲染效率。
- 分块:从微观层面提升了渲染效率。
- 合成:把图层合并到一起的操作
如何系统地优化页面?
这里说的页面优化,其实就是要让页面更快地显示和响应。
首先分析一下页面生存周期的三个阶段:加载阶段、交互阶段和关闭阶段
- 加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。
- 交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
- 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。
加载阶段
加载阶段渲染流水线如下图:
不会阻塞页面的首次渲染 比如图片、音频、视频等文件。
会阻塞首次渲染 比如JavaScript、首次请求的 HTML 资源文件、CSS 文件。 (因为在构建DOM的过程中需要HTML和JavaScript文件,在构建渲染树的过程中需要用到CSS文件)
这些能阻塞网页首次渲染的资源称为关键资源
基于关键资源,细化出三个影响页面首次渲染的核心因素:
- 关键资源个数。
- 关键资源大小
- 请求关键资源需要多少个RTT(Round Trip Time)
RTT表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。它是网络中一个重要的性能指标。
优化方案:总的优化原则就是减少关键资源个数,降低关键资源大小,降低关键资源的 RTT 次数
如何减少关键资源的个数?
- 一种方式是可以将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。
- 另一种方式,如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 async 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。当 JavaScript 标签加上了 async 或者 defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了。
如何减少关键资源的大小?
- 可以压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过前面讲的取消 CSS 或者 JavaScript 中关键资源的方式。
如何减少关键资源 RTT 的次数?
- 可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。
交互阶段
在交互阶段,帧的渲染速度决定了交互的流畅度。交互阶段渲染流水线如下图所示:
优化方案:就是让单个帧的生成速度变快。
- 减少 JavaScript 脚本执行时间
- 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
- 另一种是采用 Web Workers。Web Workers 中没有 DOM、CSSOM 环境,这意味着在 Web Workers 中是无法通过 JavaScript 来访问 DOM。
-
避免强制同步布局 是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中
-
避免布局抖动
避免重复执行计算样式和布局,尽量不要在修改DOM结构时再去查询一些相关值。
-
合理利用CSS合成动画 如果能提前知道对某个元素执行动画操作,那就最好将其标记为 will-change,这是告诉渲染引擎需要将该元素单独生成一个图层。
-
避免频繁的垃圾回收
虚拟DOM
分析DOM的缺陷:
直接操作 DOM 会触发渲染流水线的一系列反应,如果对 DOM 操作不当的话甚至还会触发强制同步布局和布局抖动的问题,这也是我们在操作 DOM 时需要非常小心谨慎的原因。
虚拟DOM怎么解决直接操作DOM所带来的问题:
- 将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上。
- 变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
- 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。
结合上图分析,虚拟DOM是怎么运行的
- 创建阶段。首先依据 JSX 和基础数据创建出来虚拟 DOM,它反映了真实的 DOM 树的结构。然后由虚拟 DOM 树创建出真实 DOM 树,真实的 DOM 树生成完后,再触发渲染流水线往屏幕输出页面。
- 更新阶段 如果数据发生了改变,那么就需要根据新的数据创建一个新的虚拟 DOM 树;然后 React 比较两个树,找出变化的地方,并把变化的地方一次性更新到真实的 DOM 树上;最后渲染引擎更新渲染流水线,并生成新的页面。
React Fiber更新机制, 核心算法是reconciliation, 由于不能解决虚拟DOM比较复杂的时候,React团队重写了reconciliation算法,新的算法称为Fiber reconciler,之前老的算法称为 Stack reconciler。
双缓存和MVC模式
- 双缓存是一种经典的思路,应用在很多场合,能解决页面无效刷新和闪屏的问题,虚拟 DOM 就是双缓存思想的一种体现。
使用双缓存,可以先将计算的中间结果存放在另一个缓冲区,等全部的计算结束,该缓冲区已经存储了完整的图形之后,再将该缓冲区的图形数据一次性复制到显示缓冲区,这样就使得整个图像的输出非常稳定。
- MVC核心思想就是:将数据和视图分离,它们之间的通信通过控制器来完成。
MVC 基础结构:
基于 React 和 Redux 构建 MVC 模型:
在该图中,我们可以把虚拟 DOM 看成是 MVC 的视图部分,其控制器和模型都是由 Redux 提供的。其具体实现过程如下:
- 图中的控制器是用来监控 DOM 的变化,一旦 DOM 发生变化,控制器便会通知模型,让其更新数据;
- 模型数据更新好之后,控制器会通知视图,告诉它模型的数据发生了变化;
- 视图接收到更新消息之后,会根据模型所提供的数据来生成新的虚拟 DOM;
- 新的虚拟 DOM 生成好之后,就需要与之前的虚拟 DOM 进行比较,找出变化的节点;
- 比较出变化的节点之后,React 将变化的虚拟节点应用到 DOM 上,这样就会触发 DOM 节点的更新;
- DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。
渐进式网页应用(PWA)
浏览器的三大进化路线:
- 第一个是应用程序Web化;
- 第二个是Web应用移动化;
- 第三个是Web操作系统话;
PWA以什么方式切入移动端的?
PWA全称Process Web APP,渐进式网页应用。字面意思就是”渐进式+Web应用“。
定义:它是一套理念,渐进式增强 Web 的优势,并通过技术手段渐进式缩短和本地应用或者小程序的距离。基于这套理念之下的技术都可以归类到 PWA。
Web应用的缺陷
- 首先,Web 应用缺少离线使用能力,在离线或者在弱网环境下基本上是无法使用的。而用户需要的是沉浸式的体验,在离线或者弱网环境下能够流畅地使用是用户对一个应用的基本要求。
- 其次,Web 应用还缺少了消息推送的能力,因为作为一个 App 厂商,需要有将消息送达到应用的能力。
- 最后,Web 应用缺少一级入口,也就是将 Web 应用安装到桌面,在需要的时候直接从桌面打开 Web 应用,而不是每次都需要通过浏览器来打开。
针对以上 Web 缺陷,PWA 提出了两种解决方案:通过引入 Service Worker 来试着解决离线存储和消息推送的问题,通过引入 manifest.json 来解决一级入口的问题。 下面我们就来详细分析下 Service Worker 是如何工作的。
什么是 Service Worker
我们先来看看 Service Worker 是怎么解决离线存储和消息推送的问题。
其实在 Service Worker 之前,WHATWG 小组就推出过用 App Cache 标准来缓存页面,不过在使用过程中 App Cache 所暴露的问题比较多,遭到多方吐槽,所以这个标准最终也只能被废弃了,可见一个成功的标准是需要经历实践考量的。
所以在 2014 年的时候,标准委员会就提出了 Service Worker 的概念,它的主要思想是在页面和网络之间增加一个拦截器,用来缓存和拦截请求。整体结构如下图所示:
在没有安装 Service Worker 之前,WebApp 都是直接通过网络模块来请求资源的。安装了 Service Worker 模块之后,WebApp 请求资源时,会先通过 Service Worker,让它判断是返回 Service Worker 缓存的资源还是重新去网络请求资源。一切的控制权都交由 Service Worker 来处理。
WebComponent
组件化
其实组件化没有明确的定义,不过可以用10个字形容什么是组件化:对内高内聚,对外低耦合。对内各个元素彼此紧密结合、相互依赖,对外和其他组件的联系少且接口简单。
CSS和DOM是阻碍组件化的两个因素
- CSS的全局属性会阻碍组件化
- 在页面中只有一个DOM,任何地方都可以直接读取和修改DOM
基于上面的原因,就出现了WebComponent,它是一套技术的组合,包含了Custom elements(自定义元素)、Shadow DOM(影子 DOM)和HTML templates(HTML 模板),详细内容可以参考MDN上的相关链接
自定义元素、影子 DOM 和 HTML 模板三种技术,使得开发者可以隔离 CSS 和 DOM。
写在最后
学习资源来自极客时间 - 李兵老师 《浏览器工作原理与实践》。接下来,让我们一起每日打卡,check完成所有课程吧 ~