React学习——React性能优化你知道多少?

144 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

因某些原因一直未能详细阅读React官网,近期仔细阅读之后发现......

性能优化

在依赖列表中省略函数是否安全?

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);  
  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}

经常习惯性的把函数写在useEffect外部,然后在useEffect内部调用,今天才发现这样写虽然不会出什么问题但是会影响性能,正确写法如下

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);    
    }
    doSomething();
  }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}

错误的数据请求方式

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop    const json = await response.json();
    setProduct(json);
  }

  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`  // ...
}

正确的数据请求方式

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    // 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
        async function fetchProduct() {
        const response = await fetch('http://myapi/product/' + productId);      
        const json = await response.json();      
        setProduct(json);    
     }
    fetchProduct();
  }, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId  // ...
}

优化过后的数据请求

这同时也允许你通过 effect 内部的局部变量来处理无序的响应:

  useEffect(() => {
    let ignore = false;    
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      if (!ignore) setProduct(json);    
    }

    fetchProduct();
    return () => { ignore = true };  
  }, [productId]);

看看 这个小 demo 和 这篇文章 来了解更多关于如何用 Hook 进行数据获取。

如果我的 effect 的依赖频繁变化,我该怎么办?

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // 这个 effect 依赖于 `count` state    
    }, 1000);
    return () => clearInterval(id);
  }, []); // 🔴 Bug: `count` 没有被指定为依赖
  return <h1>{count}</h1>; // count增加一次之后不再增加
}

指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量     
    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅ 我们的 effect 不适用组件作用域中的任何变量
  return <h1>{count}</h1>;
}

也可以使用Ref去保存state,也可以获取到最新值

function Example(props) {
  // 把最新的 props 保存在一个 ref 中  
  const latestProps = useRef(props);  
  useEffect(() => {    latestProps.current = props;  });
  useEffect(() => {
    function tick() {
      // 在任何时候读取最新的 props      
     console.log(latestProps.current);    
    }

    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []); // 这个 effect 从不会重新执行
}

我该如何实现 shouldComponentUpdate?

你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较:

function Child({seconds}){
    console.log('I am rendering');
    return (
        <div>I am update every {seconds} seconds</div>
    )
};
export default React.memo(Child)

只有当seconds发生变化时才会更新

image.png

请参考 小dome

如何记忆计算结果?

useMemo 也允许你跳过一次子节点的昂贵的重新渲染:

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

只有当a发生变化时child1||child2才会重新加载,未发生变化时采用上一次的计算结果进行渲染

如何惰性创建昂贵的对象?

第一个常见的使用场景是当创建初始 state 很昂贵时:

错误的创建state的方式
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用,多次调用会影响性能
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}
正确的创建state的方式

为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState,在函数中去调用

function Table(props) {
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

避免重新创建 useRef() 的初始值:

错误的初始化ref的方式
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都会被创建
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
正确的创建ref的方式

useRef 不会 像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:

function Image(props) {
  const ref = useRef(null);

  // ✅ IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }

  // 当你需要时,调用 getObserver()
  // ...
}