React 性能和优化

随着项目规模的增大, 性能和优化也成了不可或缺的一部分. 本文主要演示一些常用的性能提升技巧

React

Performance & Optimization

代码分割

视频推荐: Code Splitting with React, React.lazy, and React Router v5 核心概念: Don't download code until that user needs it. 需要哪一块的 code 就下哪一块(chunk)

1. 动态导入模块 import

例如下面的registerModule文件. 只有当用户需要注册时才下载这个模块的文件.

registerModule.js

const register = formData => {
  console.log(formData)
}

export { register }

App.js

import("./modules/registerModule")
  .then(module => module.register(formData))
  .catch(err => console.log(err))

2. 懒加载组件 React.Lazy

不支持服务端渲染

需要懒加载的组件被<Suspense>包起来. 其中fallback props 为等待组件加载过程中渲染的元素, 且必须包括. (fallback 可以为 null 但必须 explicit 指定)

下面的 demo 如果用户没有浏览本季排行正在热播页面, 这两个文件也不会下载. 只有当用户浏览时才下载该部分的 chunk

const Home = lazy(() => import("./pages/Home")) // import Home from './pages/Home'
const Air = lazy(() => import("./pages/Air"))
const Rank = lazy(() => import("./pages/Rank"))

const App = () => {
  return (
    <Router>
      <nav>
        <Link to="/">主页</Link>
        <Link to="/air">正在热播</Link>
        <Link to="/rank">本季排行</Link>
      </nav>

      <Suspense fallback={<Loading />}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/air" component={Air} />
          <Route exact path="/rank" component={Rank} />
        </Switch>
      </Suspense>
    </Router>
  )
}

Memorization & Rendering

1. React.memo()

组件的 memorization, 如果组件的 props 有更新则重渲染 否则利用上一次的

下面的演示用<SnowBackground />组件为一个背景渲染 1000 个粒子的雪花组件.

其中<input />每次文本输入框内更新则会使的<App />组件重新渲染. 同理<SnowBackground />组件由于是 child component 也会再渲染.

现在 1000 个粒子每次渲染也会卡顿以下, 不过最重要的是如果<SnowBackground />props没有更新, 我们也没有必要重新渲染这个组件. 随着粒子个数的增多, 频繁的渲染自然会影响用户的体验.

const SnowBackground = React.memo(() => {
  console.log("渲染雪花")
  return <Snow backgroundColor="#000" particles={1000} />
})

const App = () => {
  console.log("渲染 App")

  const handleChange = e => setSearch(e.target.value)

  return (
    <>
      <input type="text" value={search} onChange={handleChange} />
      <SnowBackground />
    </>
  )
}

export default App

2. useMemo()

的 memorization, 如果有相关的 dependencies 更新 则重新 call 函数取新的值.

1. 函数时间复杂度太高 根据依赖的变更进行重计算

calculate()是一个复杂度较高的函数, 但<button>的 toggle theme 同样使得每次渲染 App, 导致calculate再次渲染. 即使 toggle 和这个函数并没有联系.

在这里用useMemo记录这个函数的返回值, 如果number不变则下次还是用这个值, 否则再 recall calculate()函数

2. Referential Equality 记录 reference value

同理themeStyle是一个 object, 每次<App />的渲染也会重新渲染一个新的 themStyle object. 因为 js 中 object 为 reference value. 这一次跟上次的对比的 shallow 会导致 react 认为这个 object 不一样. 因此每次 input 更改时都会重新渲染.

const calculate = n => {
  for (let i = 0; i < 1000000000; i++) {}
  console.log("计算")
  return n + 2
}

const App = () => {
  const [number, setNumber] = useState(0)
  const [dark, setDark] = useState(false)
  const result = useMemo(() => {
    return calculate(number)
  }, [number])

  const themeStyle = useMemo(() => {
    return {
      backgroundColor: dark ? "black" : "white",
      color: dark ? "white" : "black",
    }
  }, [dark])

  return (
    <>
      <input
        type="number"
        value={number}
        onChange={e => setNumber(parseInt(e.target.value))}
      />
      <button onClick={() => setDark(prevState => !prevState)}>
        Change Theme
      </button>
      <div style={themeStyle}>{result}</div>
    </>
  )
}

export default App

3. useCallback()

函数的 memorization, 通常搭配 React.memo() 避免函数的重复渲染.

1. 情景 1: useCallback() + React.memo() 解决组件重复渲染

下面的代码中, 我们的子组件<Child />使用memo来避免组件的重复渲染. 同时从父级传入一个increment函数来更新计数器.

但是memo在这里并没有起到作用, 我们的预期是每次点击按钮时只有父级组件重新渲染, <Child />memo每次会对比 props 来决定是否要更新. 很明显我们的函数increment()并没有改变, 改变的只是count这个值.

这是因为 Javascript 中函数也是对象(Object), 然后又因为Referential Equality的问题, 即使两个对象所有的属性完全相同也属于两个不同的对象. 这也就是为什么memo这里依旧重复渲染子组件.

然而可以想到之前我们用useMemo解决了Referential Equality的问题, 然而

  • useMemo()返回的是
    • 例如前面例子我们用useMemo()记忆化一个时间复杂度很高的函数返回结果
  • useCallback()返回的是函数, 且可以接受参数的函数.
    • 我们用这个函数处理一些操作

因此这里我们用useCallback()memo搭配来避免函数和子组件的重复渲染.

import React, { useCallback, useState } from "react"

const Child = React.memo(props => {
  console.log("渲染子组件")

  const { increment } = props
  return (
    <>
      <button onClick={() => increment(5)}>Increment</button>
    </>
  )
})

const App = () => {
  console.log("App父级组件")
  const [count, setCount] = useState(0)

  const increment = useCallback(step => {
    setCount(prevState => prevState + step)
  }, [])

  return (
    <>
      <h1>Count: {count}</h1>
      <Child increment={increment} />
    </>
  )
}

export default App