性能
注意:这篇指南还有改进的空间。目前请将其视为参考信息。
Jotai 和 React 为我们提供了相当多的工具来管理应用生命周期中发生的重新渲染。 首先,请阅读关于渲染与提交的区别,因为在进一步了解之前,理解这个非常重要。
低成本渲染
如核心部分所述,由于 React 18 的默认行为(但总体上是好的实践),你必须确保你的组件函数是幂等的。 它们将在渲染阶段被多次调用,甚至在挂载时。所以我们需要尽可能地降低我们的渲染成本!
重计算
始终在 React 生命周期之外进行重计算(例如在操作中)
不推荐的做法:
// 每个项目的重计算const selector = (s) => s.filter(heavyComputation)const Profile = () => {const [computed] = useAtom(selectAtom(friendsAtom, selector))}
推荐的做法:
const friendsAtom = atom([])const fetchFriendsAtom = atom(null, async (get, set, payload) => {// 获取所有朋友const res = await fetch('https://...')// 只进行一次重计算const computed = res.filter(heavyComputation)set(friendsAtom, computed)})// 在组件中使用const Profile = () => {const [friends] = useAtom(friendsAtom)}
小组件
观察到的原子应该只重新渲染你的应用程序需要更新的小部分。React 需要进行的比较越少,你的渲染时间就越短。
不推荐的做法:
const Profile = () => {const [name] = useAtom(nameAtom)const [ageAtom] = useAtom(ageAtom)return (<><div>{name}</div><div>{age}</div></>)}
推荐的做法:
const NameComponent = () => {const [name] = useAtom(nameAtom)return <div>{name}</div>}const AgeComponent = () => {const [age] = useAtom(ageAtom)return <div>{age}</div>}const Profile = () => {return (<><NameComponent /><AgeComponent /></>)}
按需渲染
通常,主要的性能开销将来自于重新渲染你的应用程序不需要的部分,或者比应该的多得多。
我们有一些工具来处理 React 应该何时渲染我们的组件。如果你还没有看到 useMemo
和 useCallback
的使用,请在进一步了解之前查看官方的 React 文档以获取更多信息。
它们对于减少你的应用程序不流畅的地方的不必要渲染非常有用。
但是 Jotai 也提供了一套工具来处理我们的原子应该何时触发重新渲染。
- 开箱即用,Jotai 鼓励你将数据分解成原子部分,因此每个原子都被单独存储,并且只有在它们自己的值改变时才会触发重新渲染
selectAtom
允许你订阅大对象的特定部分,并且只有在值改变时才重新渲染focusAtom
与 selectAtom 相同,但是为部分创建一个新的原子,提供一个 setter 轻松更新该特定部分splitAtom
为动态列表完成 selectAtom/focusAtom 的工作
虽然这看起来很简单,但是它很容易理解。这就是目标,让它保持简单以保持快速。
频繁或稀有的更新
问问自己你的原子通常是否会频繁更新或更少。
让我们想象一个原子包含一个几乎每秒都在变化的对象,使用 focusAtom
来关注这个对象的特定属性可能不是最好的选择,因为无论如何它们都会在同一时间重新渲染,所以最好不要增加开销,也不要创建更多的原子。
另一方面,如果你的对象有很少改变的属性,最重要的是,这些属性独立于其他属性改变,那么你可能想要使用 focusAtom
或 selectAtom
来防止不必要的渲染。
"停止观察"模式
一个可能有趣的模式示例是使用 useMemo
在第一次渲染时只读取一次原子值,以防止即使原子在后面改变也进一步渲染。
让我们想象一个情况,你有一个切换列表。让我们看看两种方法:
标准模式
我们创建一个设置为 false 的 3 个切换的存储
const togglesAtom = atom([false, false, false])
然后,当用户点击一个切换时,我们更新它
const Item = ({ index, val }) => {const setToggles = useSetAtom(togglesAtom)const onPress = () => {// setToggles(old => [...old, [index]: !val])setToggles(old => {const newArray = [...old];newArray[index] = !newArray[index];return newArray;})}}const List = () => {const [toggles] = useAtom(togglesAtom)return toggles.map((val, index) => <Item index={index} val={val} />)}
使用这种方法,更新任何切换都会导致所有的 <Item />
重新渲染。
Memoized 模式
现在让我们尝试在第一次渲染时记住值
const togglesAtom = atom([atom(false), atom(false), atom(false)])const List = () => {const [toggles] = useAtom(togglesAtom)return toggles.map((val, index) => <Item index={index} val={val} />)}
但现在这意味着我们必须在每个 Item
中处理变化
const Item = ({ index, val }) => {const [toggle, setToggle] = useAtom(val)const onPress = () => {setToggle(!toggle) // 本地更新}}
现在如果你更新一个切换,它只会重新渲染对应的 <Item />