Suspense,一种容易被忽略的性能优化技术

3,939 阅读5分钟

前言

Suspense技术起初是为了加载异步组件,但逐渐应用到了所有异步过程中。其中,异步请求是Suspense最主要的使用场景。在不远的将来(React17/Vue3.1),基于Suspense的开发模式将成为主流,同时用户体验也会随着Suspense的使用得到优化。


Suspense的使用方式

Vue的使用方式

codesandbox.io/s/bold-cdn-…

React的使用方式

codesandbox.io/s/kind-swar…


Suspense原理

Suspense作为一个通用的技术,React和Vue在实现的思路上基本一致,核心原理有两方面:通信和渲染。

通信是指:Suspense与嵌入其内部的组件之间的通信,即Suspense如何获取组件的异步状态

渲染是指:Suspense如何控制嵌入其内部的组件的渲染

Suspense如何获取组件的异步状态

结论:通过“隐式”的获取组件异步状态所关联的promise。

表面上看来,不管是Vue还是React,我们都没有给Suspense传任何promise相关的props,那么Suspense到底是如何拿到的呢?我们固化的思维容易认为:props是通往组件的唯一入口。

实际上,Suspense作为一个框架内置的组件,它有“充分的自由”去获取任何运行时中存在的变量。

  • 对于Vue来说,如果在setup中"return"了一个promise,那么Vue就能拿到这个promise,并自动传递给离这个组件最近的那个Suspense。

  • 对于React来说,如果在render中"throw"了一个promise,那么React就能捕获到这个promise,并自动传递给离这个组件最近的那个Suspense。

这样,Suspense就能获取到组件的异步状态了。

Suspense如何控制嵌入其内部的组件的渲染

结论:预渲染嵌入的组件到一个隐藏的dom容器中,并在异步状态resolve之后把预渲染的组件移动到真实的dom容器中。

Suspense总会预渲染组件到一个隐藏的容器,之后它会根据所关联的promise的状态进行抉择:

  1. 如果promise处于pending状态,那么就会渲染fallback组件到真实的dom容器中

  2. 如果promise到达resolve状态,那么就会直接把预渲染好的组件移动到真实的dom容器中,并清除fallback组件

  3. 如果promise到达reject状态,那么框架会直接抛出reject的错误,后面可以被ErrorBoundary接收

第一阶段用户体验优化

在组件中加载异步数据是业务场景中最常见的需求,组件的状态会根据异步数据的状态不同而变化,通常是:loading状态 -> 真实内容。

使用传统的v-if/v-else,或者jsx中的三元表达式的形式,去渲染不同状态下对应的组件内容,实际会有性能问题:

// template
<Loading v-if="loading">loading...</Loading>
<Content v-else>真实内容</Content>

// jsx
(
	loading ? <Loading>loading</Loading> : <Content>真实内容</Content>
)

在切换渲染两种不同的element时,都伴随着组件的销毁与重建。而如果使用Suspense,真实内容和fallback这两者会同时存在,真实内容不会随着fallback出现而销毁,也不会随着fallback的消失而重建,相反真实内容只是在更新。

这样,用户体验就得到了第一阶段的优化,因为页面的渲染性能明显提升。


第二阶段用户体验优化(仅React)

渲染性能已经得到明显的提升,那么我们还能优化什么呢?React并发模式带来了新的思路:干掉loading状态。

React并发模式是React Fiber架构的应用,在React Fiber架构下,组件树的更新以组件为粒度被异步化,组件的更新可以被中断,从而不会阻塞主线程。

更多内容请看:reactjs.org/docs/concur…

实际环境中,用户的设备性能、网络速度各有不同,有的人性能好网速快,有的人性能差网速慢,结果就导致这种情况:

设备条件

使用体验

原因

设备条件已经这么好了,加载数据毫秒级别,但是还是要闪一下loading状态

设备条件不好,加载数据秒级别,有一个loading状态会很舒服

在React并发模式中结合使用Suspense,即可解决上述问题,这里有一个demo:

codesandbox.io/s/elated-ca…

  1. 点击Refresh会更新列表

  2. 更新列表的接口耗时随机在0-1s之间

  3. 如果接口返回的时间在0.5s之内,则不会显示loading状态

  4. 如果接口返回的时间在0.5s之外,则会显示loading状态


FAQ

Wait,我不用React并发模式也能实现第二阶段用户体验优化

是的,我们完全可以自行实现这个功能,无非是加一个定时器,只有超过某个时间后,才会显示loading。但是,这样我们就享受不到Suspense预渲染带来的性能提升了。

Vue用户体验就一定不如React吗?

虽然Vue在Suspense上无法贡献更多性能优化,但是Vue会从另外一个方向:预分析vnode,根据类型不同给予相应的patchFlag,并根据flag的不同采取最高效的更新方式,带来显著的渲染性能提升。