• 作者:老汪软件技巧
  • 发表时间: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 来开启并发更新:

策略优化的意思_极致游戏优化版win7_

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%),这也意味着带来了更好的用户体验。