Skip to content

React 文档

导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。


react 渲染优化

函数组件优化

三大缓存:缓存组件、缓存函数本身、缓存函数结果,以此减少不必要的渲染和计算开销

  1. 缓存组件 - React.memo

React.memo 是 React 提供的一个高阶组件(HOC),它会缓存组件的渲染结果,只有当 props 真正改变 时,组件才重新渲染,类似于类组件中的 PureComponent

js
import { memo } from 'react';

const Child = memo(({ name }) => {
  return <div>{name}</div>;
}, (prevProps, nextProps) => {
    // 第二个参数为 arePropsEqual:返回 true 表示 props 相当,跳过本次渲染
    return prevProps.name === nextProps.name;
});
  1. 缓存函数本身 - useCallback

避免每次渲染都创建新函数,用于传递给子组件的方法

js
import { useCallback } from 'react';

const memoizedFn = useCallback(() => {
    doSomething(a, b);
}, [a, b]);

<Child data={data} memoizedFn={memoizedFn}></Child>
  1. 缓存函数结果 - useMemo:相当于 vue 的计算属性
js
import { useMemo } from 'react';

const [num, setNum] = useState(0);
const [other, setOther] = useState(100);

const memoizedValue = useMemo(() => {
    return other + 999 + num;
}, [num, other]);

类组件优化

  • 手动比较:手动比较 state 和 props
  • 自动比较:自动 浅 比较 state 和 props
  1. 手动比较 - shouldComponentUpdate:适用于复杂组件的精细控制
js
shouldComponentUpdate(nextProps, nextState) {
    // 仅在数据变化时更新
    if (this.props.id !== nextProps.id) return true;
    if (this.state.count !== nextState.count) return true;
    return false;
}
  1. 自动比较 - PureComponent:自动浅比较,适合大部分场景,比手动实现更简洁 注意:传递对象 / 数组时需创建新引用,否则 PureComponent 可能不更新
js
class User extends React.PureComponent {
    render() {
        return <div>{this.props.name}</div>;
    }
}

react 如何提高渲染效率?避免不必要 render?

回答

shell
React 渲染效率优化的本质是:"该渲染的组件精准渲染,不该渲染的组件绝不渲染"
避免不必要的 render:通过 props/state 比较、缓存等手段,跳过不需要更新的组件。
优化 render 过程:减少单次渲染的计算量(如虚拟列表、状态拆分)。

# 具体方案
1. React.memo 包裹函数组件(最常用)
原理:React.memo 是高阶组件(HOC),会对组件的 props 进行浅比较(shallow equal)。如果 props 未变化,直接跳过渲染,复用上次的渲染结果。
场景:纯展示型组件(如列表项、卡片),或子组件渲染仅依赖 props 时。
话术:" 对于纯展示型子组件,我会用 React.memo 包裹它。这样只要父组件传递的 props 引用不变,子组件就不会重新渲染。比如一个商品列表项组件,只有当 item 数据真正变化时才会更新。"

2. useCallback 缓存函数引用(配合 React.memo)
原理:父组件重新渲染时,内部定义的函数会生成新的引用。如果直接把新函数传给 React.memo 包裹的子组件,浅比较会失效,导致子组件误渲染。useCallback 可以缓存函数引用,只有依赖项变化时才返回新函数。
场景:父组件向子组件传递回调函数(如 onClick、onChange)时
话术:" 如果父组件要给子组件传函数,我会用 useCallback 把函数包起来。比如父组件有个 handleDelete 函数,用 useCallback 缓存后,只要它的依赖项(比如 list)不变,函数引用就不变。配合子组件的 React.memo,就能避免子组件因为父组件其他状态变化而重新渲染。"

3. useMemo 缓存计算结果
原理:对于复杂计算(如大数据过滤、排序、格式化),如果每次渲染都重新执行,会浪费性能。useMemo 可以缓存计算结果,只有依赖项变化时才重新计算。
场景:处理大数组、复杂对象转换、耗时计算等。
话术:" 如果组件里有对大数组的过滤或排序,我会用 useMemo 把计算逻辑包起来。比如一个搜索过滤列表,只有当 searchText 或原始 data 变化时,才会重新计算过滤后的列表,避免每次渲染都遍历整个数组。"

4. 合理拆分组件与状态,缩小渲染范围
原理:如果把所有状态都放在一个大组件里,任何小状态变化都会导致整个组件重新渲染。通过拆分组件,把不相关的状态放到不同子组件中,可以让状态变化只影响局部。
场景:页面包含多个独立模块(如表单 + 列表、侧边栏 + 主内容)时。
话术示例:" 我会避免把所有状态堆在一个组件里。比如一个页面有表单和列表,我会把表单状态封装在 Form 组件,列表状态封装在 List 组件。这样表单输入时,只有 Form 组件重新渲染,不会影响 List。"

5. 列表渲染用唯一 key,避免用索引
原理:key React 识别列表项的唯一标识。用不稳定的 index key,当列表顺序变化时,React 会错误地复用 DOM 节点,导致额外的渲染甚至 bug。用唯一 ID 能让 React 精准复用节点。
场景:任何列表渲染(map)。
话术示例:" 列表渲染时我会用数据的唯一 ID 做 key,而不是数组索引。比如用户列表用 user.id,这样即使列表排序或插入新项,React 也能正确识别每个节点,减少不必要的 DOM 销毁和重建。"

6. 长列表用虚拟列表(Virtual List)
原理:如果渲染上千条数据,即使列表项不重新渲染,大量 DOM 节点也会占用内存。虚拟列表只渲染可视区域内的节点,滚动时动态替换内容,大幅减少 DOM 数量。
工具:react-window(轻量)、react-virtualized(功能全)。
场景:长列表、无限滚动、大数据表格。
话术示例:" 对于超过几百条的长列表,我会用虚拟列表。比如用 react-window,只渲染屏幕可见的几十条数据,滚动时再动态加载新内容。这样不管有多少条数据,DOM 节点数都保持在可控范围,渲染和滚动都会很流畅。"

# 总结
" 总的来说,React 渲染优化的核心是 ' 缓存 ' + ' 拆分':用 React.memo、useMemo、useCallback 做缓存,避免不必要的渲染;用组件拆分和虚拟列表做结构优化,减少单次渲染的压力。同时,我会用 React DevTools 的 Profiler 面板定位性能瓶颈,针对性优化,而不是盲目加优化手段。"

代码

  • React.memo —— 组件级缓存:子组件仅依赖 props,父组件其他状态变化时不希望子组件重新渲染。
js
// 子组件:用 React.memo 包裹
const ProductItem = React.memo(({ item, onBuy }) => {
  console.log('ProductItem 渲染了'); // 只有 item 或 onBuy 变化时才会打印
  return <div onClick={() => onBuy(item.id)}>{item.name}</div>;
});

// 父组件
const App = () => {
  const [count, setCount] = useState(0);
  const [product] = useState({ id: 1, name: 'iPhone' });

  // 注意:这里的 onBuy 还没优化,后面用 useCallback 解决
  const handleBuy = (id) => console.log('购买', id);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>点击次数: {count}</button>
      {/* 即使 count 变化,ProductItem 也不会重新渲染(因为 product 引用不变) */}
      <ProductItem item={product} onBuy={handleBuy} />
    </div>
  );
};
  • useCallback —— 函数引用缓存(配合 React.memo) —— 父组件向子组件传递回调函数,避免父组件渲染导致子组件误渲染。
js
const ProductItem = React.memo(({ item, onBuy }) => {
  console.log('ProductItem 渲染了');
  return <div onClick={() => onBuy(item.id)}>{item.name}</div>;
});

const App = () => {
  const [count, setCount] = useState(0);
  const [product] = useState({ id: 1, name: 'iPhone' });

  // 用 useCallback 缓存函数引用,只有依赖项(这里是空数组)变化时才返回新函数
  const handleBuy = useCallback((id) => {
    console.log('购买', id);
  }, []); // 依赖项:如果函数里用到了 product,需要把 product 放进去

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>点击次数: {count}</button>
      {/* 现在 count 变化时,handleBuy 引用不变,ProductItem 不会重新渲染 */}
      <ProductItem item={product} onBuy={handleBuy} />
    </div>
  );
};
  • useMemo —— 计算结果缓存 —— 避免每次渲染都执行耗时的计算(如大数组过滤、排序)
js
const App = () => {
  const [searchText, setSearchText] = useState('');
  const [count, setCount] = useState(0);
  
  // 模拟大数组
  const bigList = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }));

  // 用 useMemo 缓存过滤结果,只有 searchText 或 bigList 变化时才重新计算
  const filteredList = useMemo(() => {
    console.log('执行了过滤计算'); // count 变化时不会打印
    return bigList.filter(item => item.name.includes(searchText));
  }, [searchText, bigList]); // 依赖项

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>点击次数: {count}</button>
      <input value={searchText} onChange={(e) => setSearchText(e.target.value)} />
      <div>结果数量: {filteredList.length}</div>
    </div>
  );
};
  • 组件拆分 —— 把大组件拆成小组件,让状态变化只影响局部。
js
// ❌ 不好的做法:所有状态都在一个组件,输入时整个页面重新渲染
const BadApp = () => {
  const [text, setText] = useState('');
  const [count, setCount] = useState(0);
  console.log('BadApp 渲染了'); // 输入 text 或点击 count 都会打印

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
};

// ✅ 好的做法:拆分成独立小组件,状态变化只影响自身
const InputComponent = () => {
  const [text, setText] = useState('');
  console.log('InputComponent 渲染了'); // 只有输入时打印
  return <input value={text} onChange={(e) => setText(e.target.value)} />;
};

const ButtonComponent = () => {
  const [count, setCount] = useState(0);
  console.log('ButtonComponent 渲染了'); // 只有点击时打印
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

const GoodApp = () => {
  return (
    <div>
      <InputComponent />
      <ButtonComponent />
    </div>
  );
};
  • 虚拟列表 —— react-window 示例 —— 长列表只渲染可视区域,减少 DOM 节点。
js
import { FixedSizeList as List } from 'react-window';

const App = () => {
  // 模拟 10000 条数据
  const bigList = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);

  // 列表项渲染函数
  const Row = ({ index, style }) => (
    <div style={style}>
      {bigList[index]}
    </div>
  );

  return (
    // 只渲染可视区域的 10 条数据(高度 500px,每项 50px)
    <List
      height={500} // 列表总高度
      itemCount={bigList.length} // 数据总条数
      itemSize={50} // 每项高度
      width={300} // 列表宽度
    >
      {Row}
    </List>
  );
};

react 优化相关梳理

回答

shell
我从 “组件层面” “全局层面” 总结:
 组件内:用 useMemo(缓存计算结果)、useCallback(缓存函数),避免子组件不必要的重渲染;React.memo 包裹纯组件(浅对比 props,减少重渲染);
 列表优化:加唯一 key(避免 key index),虚拟列表(长列表只渲染可视区域);
 全局层面:路由懒加载(React.lazy + Suspense)、减少不必要的重新渲染(比如状态只存在需要的组件里)、避免重复请求(请求缓存)。

useMemo、useCallback、React.memo 该怎么用?什么时候会负优化?

shell
# 适用场景
子组件频繁重渲染时,用 useCallback 缓存传递给子组件的函数,useMemo 缓存复杂计算结果,React.memo 浅比较 props 阻止子组件无效渲染;

# 负优化场景
简单值(如数字 / 字符串)、轻量计算(如简单加法)不用缓存,因为缓存本身有开销,反而增加性能损耗;频繁变化的依赖(如每次渲染都变的对象)也不用,缓存会失效,白做功;③ 核心原则:只缓存 “重计算 / 重渲染” 的场景,不滥用。

React 长列表(万条数据)怎么优化?

shell
1. 核心用虚拟列表(react-window/react-virtualized),只渲染可视区的列表项,上下留少量缓冲区,避免一次性渲染万条 DOM;
2. 配合防抖节流:滚动事件做节流,避免同一时段内反复触发重计算;
3. 数据处理:大数据解析 / 筛选丢到 Web Worker,不阻塞主线程;
4. 组件优化:列表项用 React.memo 包裹,props 只传必要值,减少重渲染。

性能优化类

shell
组件层面:使用 React.memo(函数组件)/PureComponent(类组件)做浅比较,避免无意义的重渲染;拆分组件,让组件粒度更细,只渲染变化的部分;
渲染层面:列表渲染加唯一 key,避免 diff 算法误判;使用 useMemo 缓存计算结果、useCallback 缓存函数引用,防止因为函数 / 值重新创建导致子组件重渲染;
状态层面:减少不必要的状态,把状态放在最小作用域内,避免全局状态滥用;
其他手段:使用懒加载(React.lazy + Suspense)拆分代码包,减小首屏加载体积;避免在 render 中创建函数 / 对象,因为每次 render 都会生成新引用,触发子组件重渲染。

react 组件懒加载 + 错误边界

使用 React.lazy 和 Suspense 实现,特点:构建工具打包时实现代码分割

组件懒加载

js
// 使用 React.lazy 和 Suspense
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

路由懒加载

js
import { Component, Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

// 错误边界组件
class ErrorBoundary extends Component {
    state = { hasError: false };

    static getDerivedStateFromError() {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return <div>加载失败,请刷新重试</div>;
        }
        return this.props.children;
    }
}

export default function App() {
    return (
        <BrowserRouter>
            <ErrorBoundary>
                <Suspense fallback={<div>加载中...</div>}>
                    <Routes>
                        <Route path="/" element={<Home />} />
                        <Route path="/about" element={<About />} />
                    </Routes>
                </Suspense>
            </ErrorBoundary>
        </BrowserRouter>
    );
}

react 生产环境打包构建优化

  1. 代码分割:使用 React.lazy + Suspense 加载异步组件,或者使用路由懒加载
  2. Tree Shaking:保使用 ES Module,配合 Webpack、Rollup 移除未使用代码。(import & export)
  3. 代码压缩:Vite 2+ 默认使用 esbuild 进行代码压缩(比 Terser 快 10-100 倍),只需在 vite.config.js 中启用
  4. 缓存:Vite 默认 每次构建生成新的 hash 文件名

react 虚拟列表

js
// 使用 react-window 处理长列表
import { FixedSizeList } from 'react-window';

const LongList = ({ items }) => (
    <FixedSizeList
        height={500}
        width="100%"
        itemCount={items.length}
        itemSize={50}
    >
        {({ index, style }) => (
            <div style={style}>{items[index]}</div>
        )}
    </FixedSizeList>
);

react 图片优化

WebP 格式 代替 jpg/png

  1. 为何 WebP 性能好 WebP 性能好的本质是 “技术代差”(2010 年由谷歌推出)(JPEG 1992 年、PNG 1996 年)
  2. WebP 特点(相比jpg/png):文件体积、画质、原生透明、动画、渐进式加载、支持近无损、裁剪优化,按需调节
  3. WebP 优势 WebP 成为现代网页、APP、小程序等场景的 “性能首选图片格式”—— 目前主流浏览器(Chrome、Firefox、Edge、Safari 14.1+)和移动系统(Android 4.0+、iOS 14+)均已全面支持,兼容性已不再是障碍。

图片懒加载

  1. 方式一:自行封装(监听 scroll)
js
import React, { useRef, useEffect, useState } from 'react';

export default function LazyImageScroll({ src, placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', alt = '' }) {
  const imgRef = useRef(null);
  const [imageSrc, setImageSrc] = useState(placeholder);
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    const checkVisibility = () => {
      if (isLoaded) return;
      
      const rect = imgRef.current.getBoundingClientRect();
      const windowHeight = window.innerHeight || document.documentElement.clientHeight;
      
      // 图片顶部进入视口或底部进入视口
      if (rect.top <= windowHeight && rect.bottom >= 0) {
        setImageSrc(src);
        setIsLoaded(true);
      }
    };

    // 初始检查
    checkVisibility();
    
    // 添加滚动监听
    window.addEventListener('scroll', checkVisibility);
    return () => {
      window.removeEventListener('scroll', checkVisibility);
    };
  }, [src, isLoaded]);

  return <img ref={imgRef} src={imageSrc} alt={alt} />;
}
  1. 方式二:自行封装(新 IntersectionObserver API)
js
import React, { useRef, useEffect, useState } from 'react';

export default function LazyImage({ src, placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', alt = '' }) {
   const imgRef = useRef(null);
   const [imageSrc, setImageSrc] = useState(placeholder);

   useEffect(() => {
      const observer = new IntersectionObserver(
              (entries) => {
                 entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                       // 图片进入视口,加载图片
                       setImageSrc(src);
                       observer.unobserve(imgRef.current);
                    }
                 });
              },
              { threshold: 0.1 }
      );

      if (imgRef.current) {
         observer.observe(imgRef.current);
      }

      return () => {
         if (imgRef.current) {
            observer.unobserve(imgRef.current);
         }
      };
   }, [src]);

   return <img ref={imgRef} src={imageSrc} alt={alt} />;
}
  1. 方式三:使用现成库(最简单)
js
import LazyLoad from 'react-lazyload';

function MyComponent() {
    return (
        <LazyLoad height={200} offset={100}>
            <img src="https://example.com/your-image.jpg" alt="示例图片" />
        </LazyLoad>
    );
}

http 优化

开启 gzip/brotli 压缩

  1. 方式一:打包构建预压缩
js
// npm i -D compression-webpack-plugin

const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('node:zlib');

module.exports = {
  webpack: {
    plugins: {
      add: [
        // Gzip 压缩
        new CompressionPlugin({
          filename: '[path][base].gz',
          algorithm: 'gzip',
          test: /\.(js|css|html|svg)$/,
          threshold: 8192,  // 大于8KB才压缩
          minRatio: 0.8
        }),
        // Brotli 压缩
        new CompressionPlugin({
          filename: '[path][base].br',
          algorithm: 'brotliCompress',
          test: /\.(js|css|html|svg)$/,
          threshold: 8192,
          minRatio: 0.8,
          compressionOptions: { level: 11 }
        })
      ]
    }
  }
};
  1. 方式二:服务端开启
js
// express

const compression = require('compression');
const shrinkRay = require('shrink-ray-current');
const express = require('express');
const app = express();

// 先尝试 Brotli,再降级到 Gzip
app.use(shrinkRay({
  brotli: { quality: 5 },
  gzip: { level: 6 }
}));

// 或者只用 Gzip
// app.use(compression({ level: 6 }));

app.use(express.static('build'));
app.listen(3000);
  1. 另外:最后,nginx 服务器配置
shell
# 静态预压缩文件
gzip_static on;
brotli_static on;

# 动态压缩
gzip on;
gzip_types text/css application/javascript image/svg+xml;
gzip_vary on;

brotli on;
brotli_types text/css application/javascript image/svg+xml;
brotli_vary on;
  1. 注意 小文件(<8KB)压缩收益有限,可设置 threshold Brotli 压缩率更高但 CPU 消耗大,适合预压缩

缓存策略(强缓存 / 协商缓存)

HTML 不缓存或短时间缓存,其他静态资源长缓存

shell
location / {
  expires 1h;
  try_files $uri $uri/ /index.html;
}

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
  expires 365d;
  add_header Cache-Control "public, max-age=31536000, immutable";
}

CDN 分发资源

又名内容分发网络,极速:智能调度、边缘缓存、就近访问

  1. 作用:解决了传统网络中地域距离远、带宽受限等导致的访问延迟问题
  2. cdn 选择:如果你是中小企业可以使用 nginx 模拟 cdn 如果你需要 全球加速,其实更推荐用 Cloudflare、阿里云 OSS+CDN、腾讯云 CDN 等现成服务