性能优化三剑客:memo, useCallback, useMemo 详解

作者:码力无边

各位React性能调优师,欢迎来到《React奇妙之旅》的第十二站!我是你们的伙伴码力无边。在之前的旅程中,我们已经掌握了如何构建功能丰富的组件,甚至学会了使用Context API来优雅地管理全局状态。我们的应用功能越来越强大,但一个新的“幽灵”可能已经悄然出现——性能问题

React的虚拟DOM和高效的Diff算法已经为我们做了大量的优化工作。但默认情况下,当一个父组件的状态发生变化时,它会重新渲染自己以及其所有的子组件,无论那些子组件的props是否真的发生了变化。对于一个小型应用,这可能不是问题。但当你的组件树变得庞大而复杂时,这种“宁枉勿纵”的渲染策略可能会导致不必要的计算和DOM操作,从而让你的应用变得卡顿。

就像一位经验丰富的赛车手,我们不仅要会开车,还要学会如何调校引擎,让它在关键时刻爆发出最强的性能。今天,我们将认识React性能优化领域的“三剑客”:

  • React.memo: 组件的“记忆外衣”,防止因父组件重渲染而导致的不必要更新。
  • useCallback: 函数的“身份锁”,确保函数在多次渲染间保持稳定。
  • useMemo: 计算结果的“缓存器”,避免在每次渲染时重复执行昂贵的计算。

掌握这三位剑客的独门绝技,你将能够精确地控制组件的渲染时机,砍掉那些不必要的性能开销,让你的React应用如丝般顺滑。准备好为你的应用提速了吗?让我们开始吧!

第一章:问题的根源 —— 不必要的重渲染 (Re-render)

在优化之前,我们必须先理解问题出在哪里。让我们来看一个典型的“祸起萧墙”的例子。

import React, { useState } from 'react';// 一个简单的、无状态的子组件
function Greeting({ name }) {console.log(`Greeting to ${name} is rendering...`);return <h1>Hello, {name}!</h1>;
}function App() {const [count, setCount] = useState(0);return (<div><button onClick={() => setCount(count + 1)}>Increment Count: {count}</button>{/* Greeting组件的name prop是固定的,永远是"World" */}<Greeting name="World" /></div>);
}

实验:运行这段代码,然后多次点击"Increment Count"按钮。打开控制台,你会震惊地发现:
每一次你点击按钮,count状态变化,App组件重渲染,这都符合预期。但**Greeting to World is rendering...这行日志也会被一次又一次地打印出来!**

为什么?
Greeting组件的name prop明明从未改变过,它为什么也要跟着App一遍遍地重渲染?
这就是React的默认行为:父组件渲染,子组件就跟着渲染。即使Greeting组件这次渲染产生的JSX和上次完全一样,React仍然需要花费精力去调用Greeting函数,生成新的虚拟DOM,然后再和旧的进行比对(虽然比对后发现没变化,不会更新真实DOM)。

对于Greeting这样简单的组件,这点开销微不足道。但如果它是一个包含复杂计算、海量数据或深层嵌套的庞大组件,这种不必要的重渲染就会成为性能瓶颈。

第二章:第一剑客 React.memo —— 组件的“记忆体”

React.memo是一个高阶组件 (Higher-Order Component, HOC)。你可以把它想象成一件给函数组件穿上的“记忆外衣”。穿上这件外衣后,组件就会变得“聪明”:只有在它的props发生变化时,它才会重新渲染。

让我们用memo来改造Greeting组件:

import React, { useState } from 'react';// 使用 React.memo 包裹我们的组件
const MemoizedGreeting = React.memo(function Greeting({ name }) {console.log(`MemoizedGreeting to ${name} is rendering...`);return <h1>Hello, {name}!</h1>;
});function App() {const [count, setCount] = useState(0);return (<div><button onClick={() => setCount(count + 1)}>Increment Count: {count}</button><MemoizedGreeting name="World" /></div>);
}

再次实验:现在,无论你点击多少次按钮,控制台里的MemoizedGreeting to World is rendering...只会在第一次渲染时打印一次!之后,App组件的count state变化,但因为MemoizedGreetingname prop没有变,它成功地“跳过”了后续所有的重渲染。

memo的工作原理
React.memo会对组件的props进行一次浅比较 (Shallow Comparison)

  • 对于基本类型(string, number, boolean, null, undefined),它会比较值是否相等。
  • 对于复杂类型(object, array, function),它只会比较它们的引用地址是否相等,而不会深入比较内部的内容。

什么时候使用memo

  1. 当一个组件频繁地因为父组件的重渲染而重渲染,但它自身的props不常变化
  2. 当组件的渲染逻辑相对昂贵(包含复杂计算或渲染大量DOM节点)。
  3. 不要滥用!对于那些props总是在变的组件,使用memo不仅没有好处,反而会增加一次额外的props比较开销。

第三章:第二剑客 useCallback —— 函数的“稳定剂”

memo看起来很美好,但它常常会因为一个隐蔽的“敌人”而失效——函数类型的prop

让我们来看一个新的例子:

import React, { useState } from 'react';// 一个带按钮的子组件
const MemoizedButton = React.memo(function Button({ onClick }) {console.log("Button is rendering...");return <button onClick={onClick}>Click Me</button>;
});function App() {const [count, setCount] = useState(0);// 这个函数在每次App渲染时,都会被重新创建一个新的实例const handleClick = () => {console.log("Button clicked!");};return (<div><button onClick={() => setCount(count + 1)}>Increment Count: {count}</button><MemoizedButton onClick={handleClick} /></div>);
}

实验:运行这段代码并点击"Increment Count"按钮。你会发现,即使我们给Button组件穿上了memo外衣,Button is rendering...这行日志依然会在每次点击时打印

为什么memo失效了?
问题出在handleClick函数上。在JavaScript中,函数也是对象。每次App组件渲染时,const handleClick = () => {...}这行代码都会创建一个全新的函数对象
这意味着,传递给MemoizedButtononClick prop,在每一次渲染中都是一个引用地址不同的新函数。memo进行浅比较时,发现prevProps.onClick !== nextProps.onClick(因为它们的内存地址不同),于是memo认为prop发生了变化,就触发了重渲染。

useCallback登场!
useCallback Hook可以帮助我们“缓存”一个函数,确保在多次渲染之间,只要它的依赖项没有改变,它就返回同一个函数实例

改造App组件:

import React, { useState, useCallback } from 'react'; // 导入useCallback// ... MemoizedButton 定义不变 ...function App() {const [count, setCount] = useState(0);// 使用useCallback包裹函数定义const handleClick = useCallback(() => {console.log("Button clicked!");}, []); // 空依赖数组,意味着这个函数永远不会改变,只在初次渲染时创建一次return (<div><button onClick={() => setCount(count + 1)}>Increment Count: {count}</button><MemoizedButton onClick={handleClick} /></div>);
}

再次实验:现在,当你点击"Increment Count"按钮时,Button is rendering...终于不再打印了!useCallback成功地“稳定”了handleClick函数,让memo能够正常工作。

useCallback的依赖项
useEffect一样,useCallback也有一个依赖项数组。如果你的函数依赖于某些propsstate,你必须把它们加到依赖项数组里。

const handleClick = useCallback(() => {console.log(`Current count is: ${count}`);
}, [count]); // 只有当count变化时,才会重新创建一个新的handleClick函数

第四章:第三剑客 useMemo —— 昂贵计算的“缓存器”

useCallback是用来缓存函数的,而useMemo是用来缓存计算结果的。

想象一下,你有一个组件,它需要根据一个巨大的列表进行一次非常耗时的计算(比如筛选、排序、聚合)。

function ShoppingList({ items }) {// 这是一个昂贵的计算,每次渲染都会重新执行const expensiveCalculation = (data) => {console.log("Performing expensive calculation...");// 假设这里有一万行代码的复杂逻辑return data.filter(item => item.inStock).length;};const inStockCount = expensiveCalculation(items);// ... 其他state和逻辑const [count, setCount] = useState(0);return (<div><button onClick={() => setCount(count + 1)}>Rerender: {count}</button><h2>In Stock Items: {inStockCount}</h2>{/* ... 渲染items列表 */}</div>);
}

在这个组件里,即使我们只是点击"Rerender"按钮改变count这个无关的状态,expensiveCalculation这个耗时的函数也会被一遍又一遍地调用,因为每次重渲染,组件函数都会从头到尾执行一遍。

useMemo来拯救!
useMemo可以接收一个“创建”函数和一个依赖项数组。它只会在依赖项发生变化时,才重新调用创建函数,计算新的值。否则,它会直接返回上一次缓存的结果。

改造ShoppingList组件:

import React, { useState, useMemo } from 'react'; // 导入useMemofunction ShoppingList({ items }) {const [count, setCount] = useState(0);// 使用useMemo包裹昂贵的计算const inStockCount = useMemo(() => {console.log("Performing expensive calculation...");return items.filter(item => item.inStock).length;}, [items]); // 只有当items数组本身发生变化时,才重新计算return (<div><button onClick={() => setCount(count + 1)}>Rerender: {count}</button><h2>In Stock Items: {inStockCount}</h2></div>);
}

实验:现在,当你点击"Rerender"按钮时,控制台里的Performing expensive calculation...不再打印!只有当父组件传入一个新的items数组时,这个计算才会再次执行。

useMemo vs useCallback
你可以这样理解:
useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
一个缓存函数,一个缓存函数的执行结果。

总结:精确制导,避免“炮打蚊子”

今天,我们掌握了React性能优化中至关重要的三把利剑。它们的核心思想都是缓存(Memoization),通过“用空间换时间”的方式,避免不必要的重复工作。

让我们来做一个最终的总结,明确它们的使用场景:

  • React.memo(Component): 当你想跳过一个组件的重渲染,前提是它的props没有发生变化时,用它来包裹这个组件。这是优化的第一道防线。
  • useCallback(fn, deps): 当你想把一个函数prop传递给一个被memo包裹的子组件时,用它来包裹这个函数,以确保函数的引用稳定性,防止memo失效。
  • useMemo(createFn, deps): 当你的组件在渲染过程中有昂贵的计算逻辑时,用它来包裹这个计算,以确保只有在依赖项变化时才重新执行计算,从而缓存计算结果。

最后的警告:不要过度优化!
这些工具本身是有成本的(创建、缓存、比较)。对于那些渲染开销极小的组件,或者props总是在变化的组件,强行使用这些优化工具可能得不偿失。性能优化的第一原则是:先测量,后优化。在你觉得应用某个部分确实存在性能问题时,再有针对性地使用这些“三剑客”。

我是码力无边,为你追求卓越性能的精神点赞!在下一篇文章中,我们将学习如何封装我们自己的逻辑,创建自定义Hook,这是提升代码复用性和组织性的又一大利器!我们下期再会!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94970.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94970.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/94970.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

好用的电脑软件、工具推荐和记录

固态硬盘读写测试 AS SSD Benchmark https://gitee.com/qlexcel/common-resource-backup/blob/master/AS%20SSD%20Benchmark.exe 可以测试SSD的持续读写、4K随机读写等性能。也可以测试HDD的性能。 操作非常简单&#xff0c;点击Start(开始)即可测试。 体积小&#xff0c;免安…

Spring Task快速上手

一. 介绍Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑&#xff0c;无需依赖额外组件&#xff08;如 Quartz&#xff09;&#xff0c;配置简单、使用便捷&#xff0c;适合处理周期性执行的任务&#xff08;如定时备份数据、定…

函数(2)

6.定义函数的终极绝杀思路&#xff1a;三个问题&#xff1a;1.我定义函数&#xff0c;是为了干什么事情 函数体、2.我干完这件事&#xff0c;需要什么才能完成 形参3.我干完了&#xff0c;调用处是否需要继续使用 返回值类型需要继续使用 必须写不需要返回 void小程序#include …

BGP路由协议(一):基本概念

###BGP概述 BGP的版本&#xff1a; BGP-1 RFC1105BGP-2 RFC1163BGP-3 RFC1267BGP-4 RFC1771 1994年BGP-4 RFC4271 2006年 AS Autonomous System 自治系统&#xff1a;由一个单一的机构或者组织所管理的一系列IP网络及其设备所构成的集合 根据工作范围的不同&#xff0c;动态路…

mit6.031 2023spring 软件构造 笔记 Testing

当你编码时&#xff0c;目标是使程序正常工作。 但作为测试设计者&#xff0c;你希望让它失败。 这是一个微妙但重要的区别。 为什么软件测试很难&#xff1f; 做不到十分详尽&#xff1a;测试一个 32 位浮点乘法运算 。有 2^64 个测试用例&#xff01;随机或统计测试效果差&am…

【Unity开发】Unity核心学习(三)

四、三维模型导入相关设置 1、Model模型页签&#xff08;1&#xff09;场景相关&#xff08;2&#xff09;网格相关&#xff08;3&#xff09;几何体相关2、Rig操纵&#xff08;骨骼&#xff09;页签 &#xff08;1&#xff09;面板基础信息&#xff08;i&#xff09;None&…

C#语言入门详解(17)字段、属性、索引器、常量

C#语言入门详解&#xff08;17&#xff09;字段、属性、索引器、常量前言一、字段 Field二、属性三、索引器四、常量内容来自刘铁猛C#语言入门详解课程。 参考文档&#xff1a;CSharp language specification 5.0 中文版 前言 类的成员是静态成员 (static member) 或者实例成…

Total PDF Converter多功能 PDF 批量转换工具,无水印 + 高效处理指南

在办公场景中&#xff0c;PDF 格式的 “不可编辑性” 常成为效率瓶颈 —— 从提取文字到格式转换&#xff0c;从批量处理到文档加密&#xff0c;往往需要多款工具协同。Total PDF Converter 破解专业版作为一站式 PDF 解决方案&#xff0c;不仅支持 11 种主流格式转换&#xff…

[Windows] WPS官宣 64位正式版(12.1.0.22525)全新发布!

[Windows] WPS官宣 64位正式版 链接&#xff1a;https://pan.xunlei.com/s/VOYepABmXVfXukzlPdp8SKruA1?pwdeqku# 自2024年5月&#xff0c;WPS 64位版本在WPS社区发布第一个内测体验安装包以来&#xff0c;在近一年多的时间里&#xff0c;经过超过3万名WPS体验者参与版本测试…

WinExec

函数原型&#xff1a; __drv_preferredFunction("CreateProcess","Deprecated. See MSDN for details") WINBASEAPI UINT WINAPI WinExec(__in LPCSTR lpCmdLine,__in UINT uCmdShow); preferred : 更好的 __drv_preferredFunction("CreateProcess…

基于GA遗传优化的双向LSTM融合多头注意力(BiLSTM-MATT)时间序列预测算法matlab仿真

目录 1.前言 2.算法运行效果图预览 3.算法运行软件版本 4.部分核心程序 5.算法仿真参数 6.算法理论概述 7.参考文献 8.算法完整程序工程 1.前言 时间序列预测是机器学习领域的重要任务&#xff0c;广泛应用于气象预报、金融走势分析、工业设备故障预警等场景。传统时间…

Multi-Head RAG: Solving Multi-Aspect Problems with LLMs

以下是对论文《Multi-Head RAG: Solving Multi-Aspect Problems with LLMs》的全面解析&#xff0c;从核心问题、方法创新到实验验证进行系统性阐述&#xff1a;​​一、问题背景&#xff1a;传统RAG的局限性​​传统检索增强生成&#xff08;RAG&#xff09;在处理​​多维度复…

Jenkins 全方位指南:安装、配置、部署与实战应用(含图解)

一、Jenkins 安装 1.1 系统要求 基础环境&#xff1a;Java 8 或 Java 11&#xff08;推荐&#xff09;、至少 2GB 内存、10GB 以上磁盘空间 支持系统&#xff1a;Windows、Linux&#xff08;Ubuntu/CentOS&#xff09;、macOS 网络端口&#xff1a;默认使用 8080 端口&…

以国产IoTDB为代表的主流时序数据库架构与性能深度选型评测

> &#x1f4a1; 原创经验总结&#xff0c;禁止AI洗稿&#xff01;转载需授权 > 声明&#xff1a;本文所有观点均基于多个领域的真实项目落地经验总结&#xff0c;数据说话&#xff0c;拒绝空谈&#xff01; 目录 引言&#xff1a;时序数据库选型的“下半场” 一、维…

7.2elementplus的表单布局与模式

基础表单<template><el-form ref"ruleFormRef" :model"form" :rules"rules" label-width"100px"><el-form-item label"用户名" prop"username"><el-input v-model"form.username"…

PyTorch实战(3)——PyTorch vs. TensorFlow详解

PyTorch实战&#xff08;3&#xff09;——PyTorch vs. TensorFlow详解0. 前言1. 张量2. PyTorch 模块2.1 torch.nn2.2 torch.optim2.3 torch.utils.data3. 使用 PyTorch 训练神经网络小结系列链接0. 前言 PyTorch 是一个基于 Torch 库的 Python 机器学习库&#xff0c;广泛用…

在win服务器部署vue+springboot + Maven前端后端流程详解,含ip端口讲解

代码打包与基本配置 首先配置一台win系统服务器&#xff0c;开放你前端和后端运行的端口&#xff0c;如80和8080 前端打包 前端使用vue3&#xff0c;在打包前修改项目配置文件&#xff0c;我使用的是vite所以是vite.config.js。 import { defineConfig } from vite import …

Springcloud-----Nacos

一、Nacos的安装 Nacos是阿里推出的一种注册中心组件&#xff0c;并且已经开源&#xff0c;目前是国内最为流行的注册中心组件。下面我们来了解一下如何安装并启动Nacos。 Nacos是一个独立的项目&#xff0c;我们可以去GitHub上下载其压缩包来使用&#xff0c;地址如下&#x…

腾讯云重保流程详解:从预案到复盘的全周期安全防护

摘要 腾讯云针对国家级重大活动&#xff08;如进博会、冬奥会等&#xff09;提供的网络安全保障服务&#xff08;重保&#xff09;是一套系统化的主动防御体系。本文从“事前准备”“事中响应”“事后复盘”三个核心阶段出发&#xff0c;结合民生银行等典型用户的实战案例&…

单表查询-group by rollup优化

1、group by rollup基本用法 我们有时候在项目上看到group by rollup用法&#xff0c;其实就是对group by分组进行合计。 下面看一下例子 select count(1),c3 from t1 group by rollup(c3); 计划从计划中解读亦是如此&#xff0c;另外可以从结果上进行分析第21行的count其实就是…