三个可以优化的地方
避免过度多次渲染
组件会在以下情况下重新渲染
注意:例如组件组合的形式,<Test><Counter></Counter></Test>,即使Test发生了重新渲染,Counter也不会重新渲染。另外使用React这样的库或框架时,渲染指的是组件函数被调用的过程。这个过程并不一定意味着文档对象模型(DOM)实际上被更新了。它只是意味着组件的逻辑被执行,可能会重新计算组件的状态和属性。
什么是过度渲染呢?
- 无效渲染是指组件函数被调用了,但是并没有导致DOM的任何变化。
- 这通常发生在组件的状态或属性没有改变,但组件仍然被重新渲染的情况下。这种渲染是不必要的,因为它消耗了资源但没有带来任何视觉上的变化。
- 当然这在react中大部分情况下是没问题的,只有这种情况过度频繁时才会产生问题。
记忆化
组件、对象、函数都可以记忆化。
memo
工作原理
-
创建不会重新渲染的组件:
memo
用于创建一个组件,当其父组件重新渲染时,如果传入的props在两次渲染之间没有变化,那么这个被memo
包裹的组件将不会重新渲染。
-
只影响props:
memo
只对组件的props进行比较。如果一个被memo
包裹的组件在props没有变化的情况下,即使它自己的state发生了变化,或者它订阅的context发生了变化,它仍然会重新渲染。
局限性
-
在React中,每次渲染都会重新创建所有内容(包括对象和函数): 这意味着,即使组件的状态没有改变,每次渲染时,组件内部的函数和对象都会被重新实例化。
-
在JavaScript中,两个看起来相同的对象或函数实际上是不同的({} != {}): 在JavaScript中,对象和函数是引用类型。即使两个对象的内容完全相同,它们在内存中的地址也是不同的。这意味着,即使两个对象看起来一样,它们也是两个不同的实例。
-
因此,如果将对象或函数作为props传递,子组件在每次重新渲染时都会将它们视为新的props: 当父组件将对象或函数作为props传递给子组件时,由于每次渲染都会创建新的实例,子组件会认为这些props是新的,即使它们的值没有变化。
-
如果props在重新渲染之间不同,memo将不起作用:
memo
是React的一个优化技巧,它通过比较props来决定是否重新渲染组件。如果props在两次渲染之间没有变化,memo
可以防止不必要的渲染。但是,如果props是对象或函数,即使它们的值没有变化,由于它们是新创建的实例,memo
也会认为props已经改变,从而导致子组件重新渲染。
所以,我们需要对对象和函数进行memoization(记忆化),以使它们在重新渲染之间保持稳定(保持不变)。
useMemo与useCallback
工作原理
-
用于记忆化(memoize)对象和函数
-
缓存机制:
- 当你将对象或函数传递给
useMemo
或useCallback
时,这些值或函数会被存储在内存中(即被“缓存”)。在随后的重新渲染中,只要依赖项(即“输入”)没有变化,就会返回缓存的值或函数。
- 当你将对象或函数传递给
-
依赖数组:
useMemo
和useCallback
都接受一个依赖数组,这与useEffect
的工作方式类似。依赖数组用于追踪哪些值或状态影响到了记忆化的值或函数。- 如果依赖数组中的任何一个值发生变化,那么记忆化的值或函数就会被重新创建。这是通过比较新旧依赖项来实现的,只有当依赖项发生变化时,才会触发重新计算。
使用场景
-
防止不必要的渲染:
- 通过与
React.memo
一起使用,可以记忆化 props,以避免不必要的组件重新渲染。
- 通过与
-
避免每次渲染时的昂贵重新计算:
- 使用
useMemo
来记忆化那些计算成本较高的值。这样,只有在其依赖项发生变化时,才会重新计算这些值,从而节省性能。
- 使用
-
记忆化在其他 Hook 的依赖数组中使用的值:
- 当某个值被用作另一个 Hook(如
useEffect
)的依赖数组中的项时,使用useMemo
或useCallback
来记忆化这个值可以避免无限循环的副作用。例如,如果你在useEffect
的依赖数组中直接使用一个会不断变化的值,可能会导致无限循环的副作用执行。通过记忆化这个值,你可以确保只有当值实际改变时,副作用才会重新运行。
- 当某个值被用作另一个 Hook(如
案例
假设我们再App.js中有以下对象和函数,使用了useMemo与useCallback:
const [isFakeDark, setIsFakeDark] = useState(false);
const [posts, setPosts] = useState(() =>
Array.from({ length: 30 }, () => createRandomPost())
);
const handleAddPost = useCallback(function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}, []);
const archiveOptions = useMemo(() => {
return {
show: false,
title: `Post archive in addition to ${posts.length} main posts`,
};
}, [posts.length]);
<Archive
archiveOptions={archiveOptions}
onAddPost={handleAddPost}
setIsFakeDark={setIsFakeDark}
/>
注意下useMemo与useCallback使用方式的细微差别。
Archive使用memo如下所示:
const Archive = memo(function Archive({ archiveOptions, onAddPost }) {
// Here we don't need the setter function. We're only using state to store these posts because the callback function passed into useState (which generates the posts) is only called once, on the initial render. So we use this trick as an optimization technique, because if we just used a regular variable, these posts would be re-created on every render. We could also move the posts outside the components, but I wanted to show you this trick 😉
const [posts] = useState(() =>
// 💥 WARNING: This might make your computer slow! Try a smaller `length` first
Array.from({ length: 30000 }, () => createRandomPost())
);
const [showArchive, setShowArchive] = useState(archiveOptions.show);
return (
<aside>
<h2>{archiveOptions.title}</h2>
<button onClick={() => setShowArchive((s) => !s)}>
{showArchive ? "Hide archive posts" : "Show archive posts"}
</button>
{showArchive && (
<ul>
{posts.map((post, i) => (
<li key={i}>
<p>
<strong>{post.title}:</strong> {post.body}
</p>
<button onClick={() => onAddPost(post)}>Add as new post</button>
</li>
))}
</ul>
)}
</aside>
);
});
注意: 对于state的set函数来说,它不需要useCallback函数,它本身随着渲染就不会改变地址。
减小Bundle大小
Bundle与Bundle分别是什么?
代码分割
代码分割(Code splitting)是一种在现代前端工程中常用的优化技术,它允许将应用程序的代码库分割成多个小块(bundles),这些小块可以独立于主代码库进行加载。这种方法特别适用于大型应用程序,可以显著提高加载速度和性能
路由级别代码分割
转变为
Suspense是一个用于处理异步组件加载的组件。它允许你定义一个边界,当其子组件在加载过程中遇到异步操作(如数据获取)而暂停时,可以显示一个备用内容(fallback)。然后Suspense与React的lazy
函数结合使用,实现组件的懒加载。这种方式允许你在组件首次渲染时自动触发组件的加载,同时在加载过程中显示一个占位符。
因为懒加载的元素,当你切换到Login或者Pricing等页面的时候, 对应的js的代码块才刚刚从服务器下载过来,需要时间到达客户端。这个时候suspense组件就起到了作用!