对于前端应用的性能优化,大多数时候我们都是从加载流程开始优化起。

前面我有给大家整体地讲过《前端性能优化--归纳篇》,其实里面已经囊括了大多数场景下的一些性能优化的方向。

越是交互复杂、用户量大的业务,对性能的要求就越是严格。大多数的前端性能优化,都是从页面的启动和加载流程开始梳理和定位,对于功能复杂的业务来说,这样的梳理尤为重要。

注意:前面说过性能优化分为时间和空间两个角度,本文中提及的性能优化更多是指时间角度(即耗时)的优化。

# 常见的页面加载流程

其实我们在性能优化的归纳篇有简单说过,页面加载的过程其实跟我们常常提起的浏览器页面渲染流程几乎一致:

  1. 网络请求,服务端返回 HTML 内容。
  2. 浏览器一边解析 HTML,一边进行页面渲染。
  3. 解析到外部资源,会发起 HTTP 请求获取,加载 Javascript 代码时会暂停页面渲染。
  4. 根据业务代码加载过程,会分别进入页面开始渲染、渲染完成、用户可交互等阶段。
  5. 页面交互过程中,会根据业务逻辑进行逻辑运算、页面更新。

那么,我们可以针对其中的每个步骤做优化,主要包括:资源获取、资源加载、页面可见、页面可交互。

# 资源获取

资源获取主要可以围绕两个角度做优化:

  • 资源大小
  • 资源缓存

# 资源大小

一般来说,前端都会在打包的时候做资源大小的优化,资源类型包括 HTML、JavaScript、CSS、图片等。优化的方向包括:

(1) 合理的对资源进行分包。

首次渲染时只保留当前页面渲染需要的资源,将可以异步加载、延迟加载的资源拆离。通常我们会在代码编译打包的时候做处理,比如使用 Webpack 将代码拆到不同的 bundle 包中 (opens new window)

(2) 移除不需要的代码。

我们项目中常常会引入许多开源代码,同时我们自己也会实现很多的工具方法,但是实际上并不是全部相关的代码都是最终需要执行的代码,所以我们可以在打包的时候移除不需要的代码。现在基本大多数的打包工具都提供了类似的能力,比如 Tree-shaking。

除此之外,如果我们的项目较大,使用和依赖了多个不同的仓库。如果在不同的代码仓库里,都依赖了同样的 npm 代码包,那么我们可能会遇到打包时引入多次同样的 npm 包的情况。一般来说,我们在管理依赖包的时候,可以使用peerDependency来进行管理,避免多次安装依赖、以及版本不一致导致的多次打包和安装等情况。

(3) 资源压缩和合并。

代码压缩也常常是在打包阶段进行的,包括 JavaScript 和 CSS 等代码,在一些情况下也可以使用图片合并(雪碧图的生成)。通常也是使用的打包工具以及插件自带的压缩能力,开启压缩后的代码可能比较难定位,可以配合 Sorce Mapping 来进行问题定位。

除了打包时的压缩,我们在页面加载的时候也可以启用 HTTP 的 gzip 压缩,可以减少资源 HTTP 请求的耗时。

# 资源缓存

资源缓存的优化,其实更多时候跟我们的资源获取的链路有关,包括:

  • 减少 DNS 查询时间,比如使用浏览器 DNS 缓存、计算机 DNS 缓存、服务器 DNS 缓存
  • 合理地使用 CDN 资源,有效地减少网络请求耗时
  • 对请求资源进行缓存,包括但不限于使用浏览器缓存、HTTP 缓存、后台缓存,比如使用 Service Worker、PWA 等技术

其实,我们观察资源获取的链路,获取除了大小和缓存的角度以外,还可以做更多的优化,比如:

  • 使用 HTTP/2、HTTP/3,提升资源请求速度
  • 对请求进行优化,比如对多个请求进行合并,减少通信次数
  • 对请求进行域名拆分,提升并发请求数量

# 资源加载

资源加载步骤中,我们一般也有以下的优化角度:

  • 加载流程拆分
  • 资源懒加载
  • 资源预加载

# 加载流程拆分

页面的加载过程,常常分为两个阶段:页面可见、页面可交互。

前面我们讲了对资源做拆分,在页面启动加载的时候仅加需要的资源,拆分的过程则可以结合上述的两个阶段来做处理。

(1) 页面可见。

页面可见可以分为部分可见以及内容完全可见。

对于部分可见,一般来说可以做 loading 的展示或是直出,让用户知道页面正在加载中,而非无响应。

对于内容完全可见,则是用户可视区域内的内容完全渲染完毕。除此之外,当前可视范围以外的内容,则可以拆离出首屏的分包,通过预加载或是懒加载的方式进行异步加载。

(2) 页面可交互。

同样的,页面可交互也可以分为部分可交互以及完全可交互。

一般来说,组件的样式渲染仅需要 HTML 和 CSS 加载完成即可,而组件的功能则可能需要加载具体的功能代码。对于复杂或是依赖资源较多的功能,加载的耗时可能相对较长。在这样的情况下,我们可以选择将该部分的资源做异步加载。

在初始的内容加载完毕之后,剩下的资源需要延迟加载。对于页面功能完全可交互,同样依赖于分包资源延迟加载。加载流程的优化,不管是页面可见,还是页面可交互,都离不开延迟加载。

延迟加载可分为两种方式进行加载:懒加载和预加载。因此,资源懒加载和预加载也是加载流程中很重要的一部分。

# 资源懒加载

我们常说的懒加载其实又被称为按需加载,顾名思义就是需要用到的时候才会进行加载。通过将非必要功能进行懒加载的方式,可以有效地减少页面的初始加载速度,提升页面加载的性能。

常见的场景比如某些组件在渲染时不具备完整的功能,当用户点击的时候,才进行对应逻辑的获取和加载。遇到点击时未加载完成的情况下,可以通过适当的方式提示用户功能正在加载中。

资源懒加载常常也是跟资源分包一起进行,大多数前端框架(比如 Vue、React、Angular)也都提供了懒加载的能力,也可以配合 Webpack 打包 (opens new window)做处理。

# 资源预加载

资源预加载也称为闲时加载,很多时候我们可以在页面空闲的时候,对一些用户可能会用到的资源做提前加载,以加快后续渲染或者操作的时间。

仔细一看,资源预加载和资源懒加载都比较相似,都会通过将资源拆离的方式做成异步延迟的方式加载。两者的区别在于:

  • 懒加载的功能只会在需要的时候才进行加载,因为一些功能用户可能不会使用到,比如帮助中心、反馈功能等等
  • 预加载的功能则是在不阻塞核心功能的时候,尽可能利用空闲的资源提前加载,这部分的功能则是用户很可能会使用到,比如获取下一屏页面的内容数据

# 复杂场景下的加载流程

在页面到达可交互状态之后,后续的加载流程也可以根据业务场景做后续的优化。对于一些复杂的业务,我们可以结合业务的特点做更进一步的性能优化。

# 复杂加载流程管理

对于页面初始化流程过于复杂的应用来说,我们可以对加载流程做任务的拆分,分阶段地进行加载。

举个例子,假设我们需要在 Web 端加载 VsCode,那么我们可能需要考虑以下各个功能的加载:

- 整体页面框架
- 顶部菜单栏
- 左侧工具栏
- 底部状态栏
- 文件目录栏
- 文件详情
  - 内容展示
  - 编辑功能
  - 菜单功能
- 搜索功能
- 插件功能

以上只是我按照自己想法粗略拆分的功能,我们可以简单分成几个加载阶段:

  1. 页面整体框架加载完成。此时可以看到各个功能区域的分布,包括顶部菜单栏、左侧工具栏、底部状态栏、项目内容区域等等,但这些区域的内容未必都完全加载完成。
  2. 通用功能加载完成。比如顶部菜单栏、左侧工具栏、底部状态栏等等,一些具体的菜单或是工具的功能可以做按需加载和预加载,比如搜索功能。
  3. 项目内容相关框架加载完成。此时可以看到项目相关的内容区域,比如文件目录、当前文件的内容详情等等。
  4. 插件功能。用户安装的插件,在核心功能都加载完成之后再获取和加载。

当我们根据项目的具体加载过程做了阶段划分之后,则可以将我们的代码做任务拆分,可以拆分成串行和并行的任务。串行的任务比如按照阶段划分的大任务,并行的任务则可以是某个阶段内的小任务,其中也可以包括一些异步执行的任务,或是延迟加载的任务。

# 长耗时任务的拆离

如果我们的应用中会有耗时较长的计算任务,比如拉取回来的数据需要计算处理后才能渲染,那么我们可以对这些耗时较长的任务做任务拆分。

同样的,我们还是回到 Web 端加载 VsCode 的场景。假设我们在加载某个特别大的文件,则可以考虑分别对该文件的内容获取、数据转换做任务拆分,比如分片获取该文件的内容,根据分片的内容做渲染的计算,计算过程如果耗时较长,也可以做异步任务的拆分,甚至可以结合 Web Worker 和 WebAssembly 等技术做更多的优化。

# 读写分离

对于交互复杂、需要加载的资源较多的情况下,如果用户的权限只是可读,那么对于编辑相关的功能可以做资源拆离,对于有权限的用户才进行编辑能力的加载。

读写分离其实属于资源拆分的一种具体场景,我们可以结合业务的具体场景做具体的功能拆分,比如管理员权限相关的管理功能,也是类似的优化场景。

# 结束语

我们做性能优化的场景,更多时候出现在我们的应用出现了性能瓶颈的时候。大多数情况下,前端应用都相对简单,也无需做过度的优化。

对于复杂的应用,对加载流程和链路的梳理、划分,不管是对我们做架构设计来说,还是对于做性能优化来说,都有不小的帮助。只有理清楚整个应用的加载流程,结合对每个步骤和阶段的耗时统计,我们可以针对性地对耗时较长的地方做优化。

部分文章中使用了一些网站的截图,如果涉及侵权,请告诉我删一下谢谢~
温馨提示喵