- 作者:老汪软件技巧
- 发表时间:2024-08-25 04:01
- 浏览量:
Jotai 作为原子化的状态管理库真是越来越火了,并且随着另一个原子化状态管理库 Recoil 的成员被 Facebook 裁员之后,Jotai 似乎也成为了原子化方案的最佳选择,接下来就让我们来看一下如何在 Jotai 之上达到更加极致的性能/用户体验。
本文会围绕 jotai-scheduler 库进行讲解,jotai-scheduler 库项目地址:/jotaijs/jot… 可以给该项目点一个 Star 方便项目中使用时可以快速找到该优化方案。
传统 Jotai 或者其它状态管理库有什么问题?
假设我们现在有这样一个页面:
它包含了一个应用中通用的元素 —— Header、Footer、Sidebar、Content。每一个元素代表了一个组件,在这些组件中可能会复用同一个全局状态。
对应代码可能是这样的:
const anAtom = atom(0);
const Header = () => {
const num = useAtomValue(anAtom);
return <div className="header">Header-{num}div>;
};
const Footer = () => {
const num = useAtomValue(anAtom);
return <div className="footer">Footer-{num}div>;
};
const Sidebar = () => {
const num = useAtomValue(anAtom);
return <div className="sidebar">Sidebar-{num}div>;
};
const Content = () => {
const [num, setNum] = useAtom(anAtom);
return (
<div className="content">
<div>Content-{num}div>
<button onClick={() => setNum((num) => ++num)}>+1button>
div>
);
};
即当我们点击按钮时会更新全局状态,并触发所有的组件 re-render,为了模拟更真实的场景,我们手动来模拟繁重的渲染计算过程:
const simulateHeavyRender = () => {
const start = performance.now();
while (performance.now() - start < 500) {}
};
并在组件中进行调用:
const anAtom = atom(0);
const Header = () => {
simulateHeavyRender();
const num = useAtomValue(anAtom);
return <div className="header">Header-{num}div>;
};
const Footer = () => {
simulateHeavyRender();
const num = useAtomValue(anAtom);
return <div className="footer">Footer-{num}div>;
};
const Sidebar = () => {
simulateHeavyRender();
const num = useAtomValue(anAtom);
return <div className="sidebar">Sidebar-{num}div>;
};
const Content = () => {
simulateHeavyRender();
const [num, setNum] = useAtom(anAtom);
return (
<div className="content">
<div>Content-{num}div>
<button onClick={() => setNum((num) => ++num)}>+1button>
div>
);
};
也就是说在渲染每一个组件时都会包含了 500ms 的延迟,这时候看看当我们点击按钮时的效果:
可以看到,当我们点击按钮时,需要等较长的时间才能看到状态的变化,并且 、、、 引用的状态同一时间发生了变化。
对应打开 Chrome Performance:
可以看到此时包含了 3 个 Long Task,对应点击三次按钮,这无疑对于用户体验是非常差的。
可能现在有的小伙伴会说 React18 不是有并发更新吗?为什么不开启并发更新来解决这个问题!
好!现在我们使用 useTransition 来开启并发更新:
const Content = () => {
simulateHeavyRender();
const [num, setNum] = useAtom(anAtom);
const [isPending, startTransition] = useTransition();
return (
<div className="content">
<div>Content-{num}div>
<button
onClick={() => {
startTransition(() => {
setNum((num) => ++num);
});
}}
>
+1
button>
div>
);
};
此时效果:
对应 Chrome Performance:
可以看到,即使开启了并发更新,用户仍然需要等待同样的时间来看到状态的更新,和上面的区别仅仅是一个长任务被拆分为了数个子渲染任务。由于每个组件渲染时间是固定的,因此无论何种方式都无法减少总的渲染时长,我们唯一能做的,就是让每个组件渲染完立刻呈现给用户,而区分组件渲染先后顺序我们称之为“渲染优先级”。
通常来说用户更关心内容区域,因此我们可以假设在现在的页面中渲染优先级为:Content > Sidebar > Header = Footer,也就是说我们应该先把 组件渲染出来之后立即呈现给用户,这样就大大提前关键内容展示给用户的时机,带来更好的性能以及用户体验!
那我们怎么能做到这一点呢?答案就是 jotai-scheduler。
基于 jotai-scheduler 库进行优化
jotai-scheduler API 和 原生 Jotai 极为相似,对应关系为:
使用上唯一和 Jotai 的一点区别是可以额外传入 priority 代表渲染优先级:
import { LowPriority, useAtomValueWithSchedule } from 'jotai-scheduler'
const [num, setNum] = useAtomWithScheduleanAtom, {
priority: LowPriority,
});
const num = useAtomValueWithSchedule(anAtom, {
priority: LowPriority,
});
priority 可以传入三个值 —— ImmediatePriority, NormalPriority, LowPriority。priority 是一个可选项,如果不传入默认为 NormalPriority,此时的行为等同于原生 Jotai API。因此,现在你完全可以使用这些 API 替换掉原生 Jotai API,即使你不使用渲染优先级的能力。
好,现在让我们来看一下效果:
const anAtom = atom(0);
const Header = () => {
const num = useAtomValueWithSchedule(anAtom, {
priority: LowPriority,
});
return <div className="header">Header-{num}div>;
};
const Footer = () => {
const num = useAtomValueWithSchedule(anAtom, {
priority: LowPriority,
});
return <div className="footer">Footer-{num}div>;
};
const Sidebar = () => {
const num = useAtomValueWithSchedule(anAtom);
return <div className="sidebar">Sidebar-{num}div>;
};
const Content = () => {
const [num, setNum] = useAtomWithSchedule(anAtom, {
priority: ImmediatePriority,
});
return (
<div className="content">
<div>Content-{num}div>
<button onClick={() => setNum((num) => ++num)}>+1button>
div>
);
};
此时效果:
对应 Chrome Performance:
可以看到,重要的内容更早的展现给了用户(时间缩短 75%),这也意味着带来了更好的用户体验。