React useTransition在性能优化中的使用

发布 : 2023-07-05 分类 : React

原文链接:https://github.com/taoliujun/blog/issues/19

useTransition is a React Hook that lets you update the state without blocking the UI.

文档中简单一句话说明useTransition的用途:不阻塞UI的情况下更新状态

解决什么问题?

正常代码下,JavaScript是单线程的,所以执行一段耗时的代码,会阻塞UI的渲染,导致页面卡顿。React提供了时间切片的功能来尽量确保一帧中有充足的时间来渲染UI,而useTransition就是在这个基础上,可以在不阻塞UI的情况下使用时间分片特性更新状态

一个例子

先看下卡顿是如何形成的,一个简单的代码,每500ms,更新name状态。另外点击按钮的时候,更新一系列状态并渲染到dom中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
const getDatas = () => {
const datas = [];
for (let i = 1; i <= 2000; i += 1) {
const s = Math.random() * Math.random();
datas.push(s);
}
return datas;
};

const Main: FC = () => {
const [name, setName] = useState('world');

const [datas1, setDatas1] = useState<number[]>([]);
const [datas2, setDatas2] = useState<number[]>([]);
const [datas3, setDatas3] = useState<number[]>([]);
const [datas4, setDatas4] = useState<number[]>([]);
const [datas5, setDatas5] = useState<number[]>([]);
const [datas6, setDatas6] = useState<number[]>([]);
const [datas7, setDatas7] = useState<number[]>([]);
const [datas8, setDatas8] = useState<number[]>([]);

const onClick1 = useCallback(() => {
setDatas1(getDatas());
setDatas2(getDatas());
setDatas3(getDatas());
setDatas4(getDatas());
setDatas5(getDatas());
setDatas6(getDatas());
setDatas7(getDatas());
setDatas8(getDatas());
}, []);

useEffect(() => {
window.setInterval(() => {
setName(`world ${Math.random()}`);
}, 500);
}, []);

return (
<div>
hello {name}
<br />
<button onClick={onClick1}>click me</button>
<br />
<div>
{datas1.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas2.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas3.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas4.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas5.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas6.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas7.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
<div>
{datas8.map((v) => {
return <div key={v}>{v}</div>;
})}
</div>
</div>
);
};

首先不点击按钮,观察5秒,没有卡顿现象,性能表现如图:

可以看到有几个微微凸起的黄色点,对应着每次的hello状态更新和渲染,它们的执行时间都在1ms,没有超过一帧的时间。

选取其中一个黄色的点,查看它的详情。React的调度器、协调器、渲染器创建了对应的任务,分步执行了任务,具体可阅读React架构的相关文章。

然后连续点几次按钮,hello的渲染出现明显的卡顿,性能表现如图:

在性能图中截取的一段时间中,黄色是脚本执行时间,灰色是UI渲染时间,白色是空闲时间(我停止点击了一会儿),在每帧里,要跑完所有的状态变更和UI渲染,datas系列的状态变更和渲染占据了大量的时间,基本是阻塞了hello的状态变更和渲染。

只点击一次,性能表现如图:

可以看到几个Task,第一个Task就是在更新datas系列状态和渲染,它占据了太多帧的时间,导致hello的状态变更和渲染被推迟到后面的帧。

优化它

前面说到,React的架构中实现了时间切片,它允许开发者将不重要的变更推迟到后面的帧,这样就可以尽量保证优先执行默认任务。使用useTransition改下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const [pending, startTransition] = useTransition();

const onClick1 = useCallback(() => {
startTransition(() => {
setDatas1(getDatas());
setDatas2(getDatas());
setDatas3(getDatas());
setDatas4(getDatas());
setDatas5(getDatas());
setDatas6(getDatas());
setDatas7(getDatas());
setDatas8(getDatas());
});
}, []);

再次连续点击按钮,卡顿现象明显减轻很多,性能表现如下:

一看起来,执行时间还是很长,那么为什么hello渲染看起来不卡顿呢?

只点一次,看看性能表现:

查看几个Task的详情,发现datas系列状态的更新,被分配在了多个Task中,中间还穿插了hello的状态更新的任务。这也印证了useTransition的实现背景:将不重要的任务通过时间切片架构,分配到多帧中,优先执行其他任务,从而实现不卡顿的目的。

注意事项

  • useTransition是个hook,它的返回值还包括了一个pending状态,用来表示是否处于时间切片的过程中,可以用来优化UI,比如显示一个loading。

  • 你也可以使用startTransition这个util函数代替hook的使用。

  • 时间切片架构是调度状态变化的,所以startTransition的入参函数里,将状态更新标记为可切片,普通的代码段不会被标记。所以简单的说,你还是得将一个状态变更的执行时间控制在5ms内。