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