useDeferredValue如何正确使用

153 阅读7分钟

image.png React18中新增了useDeferredValue hook,想必大家多多少少已经看过它的文档,不过真正的理解它以及在什么场景下去使用它,并没有想象中那么简单。

本篇文章会阐述以下几点:

  1. useDeferredValue的使用
  2. useDeferredValue如何对渲染进行性能优化
  3. useDeferredValue与防抖的区别
  4. 如何结合防抖在项目中既能完成渲染的性能优化也能做到请求的限流

一、使用useDeferredValue

引用文档的话~

useDeferredValue is a React hook that lets you defer updating a part of the UI

通过这样的钩子我们可以延迟更新部分UI视图。

const deferredValue = useDeferredValue(someState)

简单来说,useDeferredValue会返回一个延迟的deferredValue值,这个值会落后于页面的someState,当我们把这样延迟的值用于某个组件渲染的话,会发现该组件更新会慢于其他视图部分~

看完这样的介绍,大家可能感觉这个hook并没什么卵用啊,我为什么要拿到个延迟的值?我为什么要延迟渲染部分UI?

其实在大多数场景下,你都不会用到这个hook, 只有特定的时候需要用到。我归纳了主要有两个场景~

1.不希望内容出现塌陷,维持原来旧的内容

想象下有这样的需求,我们搜索一篇文章,文章的加载是需要时间的,当新的文章还未出现的时候,我们的容器会塌陷,当然你也可以用loading or 别的方式撑开。

那么这个时候另外个方案就是使用useDeferredValue,它可以让你的文章继续保持上次你搜索的样子,直到新的文章加载完成才会替换。

这个场景的案例在官网是有的,这里我们不再赘述,因为说实话这个场景用不用貌似也不重要。

2.当你的组件渲染十分缓慢,并且影响到页面其他处的交互行为

这个场景是我们本篇文章重点阐述的部分,也会使用demo为大家进行讲解

二、useDeferredValue进行性能优化

现在我们有这样的场景,一个input输入框(用于搜索),下方组件list用于展示内容,但是这个list的渲染是十分缓慢的,每次用户的输入都会导致list发生渲染,从而让用户的输入事件变的非常卡顿(需要等待list渲染完成)。

这个就是典型的需要性能优化的场景,在官网中放出来的就是这样的案例~这里贴出来

React官网性能优化案例

核心的代码就是

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

注意:这里的SlowList组件必须是memo进行包裹,否则渲染的性能优化会失效。

这里我们着重思考下why?

首先使用memo包裹,会去对比组件属性text前后是否发生变化,如果不变化,那么是不会去渲染的,不过按道理我们使用了useDeferredValue,就会开启后台的更新并且延迟slowList的渲染,哪怕不去使用memo,slowList的每次渲染也应该都在后台完成呀?为什么还是会渲染卡顿呢?

问题就出在刚刚疑问中的每次渲染也应该在后台完成,我们看看官方文档中是如何介绍deferred value 在初始化、更新两个阶段做了什么吧。

During the initial render, the deferred value will be the same as the value you provided.

During updates, the deferred value will "lag behind" the latest value. In particular, React will first re-render without updating the deferred value, and then try to re-render with the newly received value in background.

文档中表示,在更新阶段,React首先会用还未更新的延迟值进行重渲染,然后才在后台用新的value值渲染,并不是说每次的渲染都发生在后台。

所以当我们不使用memo进行优化的话,哪怕你后台开启了re-render ,但是在更新阶段slowList会先用延迟值进行重渲染,在这一步我们的页面就发生了卡顿,从而导致整个性能优化失败。

我们必须要用memo解决掉这样的问题,这样我们就能保证当属性值不变化的时候,不会发生重渲染,而属性发生变化的时候,我们的重渲染是在后台完成,并且后台的重渲染是可被打断的。

三、与防抖的区别

现象上看,它们都做了延迟某些行为的发生,从而实现性能的优化,但是实际上还是有本质上的不同:

  1. 防抖是需要一个固定的延迟时间,譬如1秒后再处理某些行为,但是useDeferredValue并不是一个固定的延迟,它会根据用户设备的情况进行延迟,当设备情况好,那么延迟几乎是无感知的。这样看来useDeferredValue其实更合理些。
  2. 防抖本质上其实是解决不了渲染卡顿的问题的,他只是推迟了组件的渲染,我们试想下这样的情况,慢组件目前已经处于了渲染的阶段,此时用户再做页面输入的操作,同样是会卡住的。而useDeferredValue是在后台进行重渲染,用户触发的行为会打断后台的更新,并不会阻塞用户的输入行为。
  3. useDeferredValue本质上将页面部分ui渲染优先级变低了,它可以处理渲染出现的问题,但是没有办法处理请求限流,当用户的输入发出请求,那么此时的useDeferredValue是没办法优化它。

四、useDeferredValue结合防抖共同实现性能优化

那么官网中的案例是否能做到既能限制请求数又能实现渲染不卡顿的方案呢?

答案是有的,简单来说就是把防抖和useDeferredValue结合起来使用。

首先,我们实现一个useDebounce的自定义hook,目标就是延迟请求的发出

// useDebounce
import { useRef } from "react";
export const useDebounce = (delay, fn) => {
  const ref = useRef({ timer: false });
  return (query) => {
    if (ref.current.timer) {
      clearTimeout(ref.current.timer);
    }
    ref.current.timer = setTimeout(async () => {
      await fn(query);
      ref.current.timer = false;
    }, delay);
  };
};

在app.js中我们引入了useDebounce,让请求在用户停止输入4秒后才执行,达到限流的目的。同时,我们把请求拿到的结果通过useDeferredValue处理,让slowList慢组件的渲染优先级变低,解决渲染卡顿问题。

// app.js
import { useState, useDeferredValue } from "react";
import SlowList from "./SlowList.js";
import { requestApi } from "./mockRequest.js";
import { useDebounce } from "./useDebounce.js";
export default function App() {
  const [text, setText] = useState("");
  const [searchResult, setSearchResult] = useState("");
  const deferredText = useDeferredValue(searchResult);
  console.log(text, "input text");
  const fn = async (query) => {
    console.log(`${query}:发起请求`);
    const data = await requestApi(query);
    setSearchResult(data);
  };
  const debounceFn = useDebounce(4000, fn);
  const getResult = (e) => {
    setText(e.target.value);
    debounceFn(e.target.value);
  };
  return (
    <>
      <input value={text} onChange={(e) => getResult(e)} />
      <SlowList text={deferredText} />
    </>
  );
}

这样改造后就可以实现我们的目标了,完整的改造案例在codeSand如下:

[根据React官网改造的案例] codesandbox.io/s/usedeferr…

总结:

useDeferredValue本质上解决的是渲染问题,它允许开发者将部分渲染的优先级降低,并且实现渲染可以被打断~

在上述的性能优化案例中,我们使用了多个优化手段,memo + 防抖 + useDeferredValue, 这三个手段解决了不同的问题

memo: 解决的是组件需不需要渲染的问题

防抖:解决的是请求限流的问题,推迟部分行为的执行,这里的行为指请求

useDeferredValue: 解决的是渲染的问题,让慢组件渲染优先级变低,可打断

最后,我们的案例想要做到更好,还需要去解决请求时序的问题,因为网络请求的响应并不是按照请求的顺序到达的,

当然这不是本篇文章的重点,只是想表明下,一个简单的案例想要做到很好,是需要很多知识点的。

只有理解了这些关键知识点,我们才能更好的使用API在合理的场景下进行性能的优化,切记,不要盲目的去使用useDeferredValue.

如果大家有想了解的Hook解析或者疑问,欢迎留言讨论或私信。如果大家对源码感兴趣,可以移步到另外一个专栏:渐进式解剖React