转载自前端备忘录-江湖术士

二话不多说,先上代码:

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
import {
createContext,
Profiler,
useContext,
useEffect,
useState,
} from "react";

const Service = createContext(null);

function useService() {
const [check, setCheck] = useState("");
useEffect(() => {
setTimeout(() => {
setCheck("change");
}, 3000);
}, []);
return { check, setCheck };
}

function ChangeSome() {
const service = useContext(Service);
return (
<div
onClick={() => {
service.setCheck("checked");
}}
>
测试
</div>
);
}

function TestDeepCompo(props) {
return (
<div data-count={props.count}>
{props.count > 0 ? (
<TestDeepCompo count={props.count - 1} />
) : (
// <Provider>
<Profiler id="local" onRender={console.log}>
<ChangeSome />
</Profiler>
// </Provider>
)}
</div>
);
}

function Provider(props) {
const service = useService();
return <Service.Provider value={service}>{props.children}</Service.Provider>;
}

function App() {
return (
<div className="App">
<Provider>
<Profiler id="app" onRender={console.log}>
<TestDeepCompo count={1000} />
</Profiler>
</Provider>
</div>
);
}

export default App;

大家可以新建一个项目试一下,这里为了图方便写了高阶组件,不过还是建议大家不要用高阶组件,IOC 本身就是为了消灭高阶组件,继承,而出现的

这是一个查看 React 深度变更性能的代码,在千层深度组件中:

深度变更性能的代码

结果是,在我的电脑上,统计一下时间:

初始化各个 Fiber 节点

local: 1.89 ms app: 53 ms

更新

local: 0ms app: 2ms

多花费了 50ms 2ms 左右的时间,20ms 之后生效

注意,这个部分,不可被优化,大家可以试试,使劲浑身解数,包括但不限于 React.memo,useMemo,或者按照官方最佳实践,来只用上下文传递 setter

是的,很多人说,这个变更检测毫无意义,因为我只在最底层修改了数据,你只需要定位依赖,然后检测最底层就好

也就是说,这极端情况下的 20 ms,避免不了

会在性能监控上留下这样的痕迹:

性能监控1

然后,熟悉 ng platform-webworker 的同学首先坐不住了,不是说提倡 120 hz 高刷么?提交变更必须控制在 4ms 以内么?不然为啥上多线程?

Vue 同学也坐不住了,为啥不把变更检测控制在组件级别?我还能手动 markDirty,手动 changeDetect ,别说千层,万层我也不怕

淡定,这个是协调 m-vm(p) 的消耗,视图提交时是完整的,也就是说,这个操作会在 20 Ms 2ms 后,提交视图渲染,在 4ms 左右之后(一般的 patch 生效时间,嵌套太多可能会久一点),被用户看到

用户看到时,并不会觉得卡顿

ng 同学说不对!我 ng 一个 requestAnimationFrame 调度,来组织个动画,你的视图也需要同步的话,这 20ms ,简直要了我的命!而且 React 干嘛侵占本该我 大 rx 用的调度频道?

先别急,首先,这 20 ms 是极端情况,初始化,一般也就 几毫秒 的样子

ng 同学说:不行!几毫秒也不行,我是高刷屏+safari,react 调度不行的,还是让 rx 来吧!

急吼吼干啥?首先,你既然是流调度,加个 debounceTime 再和 react 同步不行么?只要保证这个 time 盖过了 react 的 调度时间,不就可以么?

ng 同学说:是吼!但是这样不丝滑!

而且,你就算是用 rx 调度,你就以为逃得过 react 的调度魔爪么?你难道就可以完全不用 context 么?

ng 同学说:还是不丝滑!

额,加个 transition?css 解决如何?

然后 ng 同学的处女座毛病就犯了,觉得这样不纯粹,没有真正解决问题云云

这些场景都太极端了!

追求这些极端情况的思想本身就需要调节,这样是不对的

我们写代码,遇到这样极端的情况很罕见,不需要为了极端情况而改变架构和降低使用体验

比起这几十毫秒的时间,你还是多想想如何保证代码质量吧


而且,如果你用 Context 真的出现了肉眼可见的性能问题,建议还是先查看代码中是不是没有贯彻惰性初始化,这也是为数不多的强制要求

惰性初始化才能让你感受到肉眼可见的延迟,1000 层已经是极限,再多框架可就撂挑子了

框架实现得怎么样,那是框架的事情,我们写代码写得怎么样,才是我们能控制的事情

你说 fiber 不行,源代码乱得一匹,调度太烂,还是 ng 大多线程好

是是是,反正我啥都用,也不用跟你生气

但是看着我 ——

你对 React DDD 这些简洁的写法,难道真的不动心么?

几毫秒而已,让它去吧

对了,可能会有人说这个例子还不够极限,那我们再改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function TestDeepCompo(props) {
return (
<div data-count={props.count}>
{props.count > 0 ? (
<>
<TestDeepCompo count={props.count - 1} />
<ChangeSome />
<ChangeSome />
<ChangeSome />
<ChangeSome />
<ChangeSome />
</>
) : (
// <Provider>
<Profiler id="local" onRender={console.log}>
<ChangeSome />
</Profiler>
// </Provider>
)}
</div>
);
}

深度变更性能的代码1

这下够极限了么?

1000 层级,每一层级都有响应式变量,有 5 个

变更检测多花 91 ms

性能监控2

91ms,还是没有到体感程度

但是如果我们加个其他逻辑

未惰性初始化
性能监控2
时间1
惰性初始化
性能监控3
时间2

可以看到,一个惰性初始化,把 调度 时间 从 740 水平,直接降到了 94

所以,还是希望大家搞清楚,最大消耗的源头,在哪里

不要死抓着那些地方不放

有惰性初始化,是,你的 context 还是会有意外,但是这些意外能干啥?就算千层下,你做好惰性初始化,也能把时间压到百毫秒内

他只是新建 workInProgress,你 memo 没变化,他不会动