MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 性能优化技巧:提升应用加载速度

2023-09-127.5k 阅读

使用 React.memo 进行组件优化

在 React 应用中,大量的组件渲染是导致性能问题的常见原因。其中,不必要的重新渲染会浪费大量的计算资源,从而影响应用的加载速度。React.memo 是 React 提供的一个高阶组件,它可以帮助我们避免不必要的组件重新渲染。

React.memo 用于对函数式组件进行浅比较优化。当组件的 props 没有发生变化时,React.memo 会阻止组件的重新渲染。

代码示例

import React from 'react';

// 未使用 React.memo 的组件
const MyComponent = ({ data }) => {
  console.log('MyComponent 渲染');
  return <div>{data}</div>;
};

// 使用 React.memo 的组件
const MemoizedComponent = React.memo(({ data }) => {
  console.log('MemoizedComponent 渲染');
  return <div>{data}</div>;
});

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>点击增加计数</button>
      <MyComponent data="未优化组件" />
      <MemoizedComponent data="优化后组件" />
    </div>
  );
};

export default App;

在上述代码中,MyComponent 是一个普通的函数式组件,每次父组件重新渲染时,它都会重新渲染,无论其 props 是否发生变化。而 MemoizedComponent 使用了 React.memo,只有当 data 属性发生变化时,它才会重新渲染。通过点击按钮增加 count 来触发父组件 App 的重新渲染,可以观察到 MyComponent 每次都会重新渲染并打印日志,而 MemoizedComponent 由于 props 未改变,不会重新渲染。

需要注意的是,React.memo 进行的是浅比较。如果 props 是复杂对象,比如对象或数组,即使对象内部的值发生了变化,但对象的引用没有改变,React.memo 也不会触发组件重新渲染。例如:

import React from 'react';

const ComplexComponent = React.memo(({ complexData }) => {
  console.log('ComplexComponent 渲染');
  return <div>{JSON.stringify(complexData)}</div>;
});

const App2 = () => {
  const [complexState, setComplexState] = React.useState({ value: '初始值' });
  const handleClick = () => {
    // 这里直接修改对象内部值,引用未变
    complexState.value = '新值';
    setComplexState({ ...complexState });
  };
  return (
    <div>
      <button onClick={handleClick}>点击修改复杂数据</button>
      <ComplexComponent complexData={complexState} />
    </div>
  );
};

export default App2;

在这个例子中,点击按钮修改 complexState 的内部值,但由于对象引用没有改变,ComplexComponent 不会重新渲染。为了确保在这种情况下组件能够正确重新渲染,可以使用一些方法来创建新的对象引用,比如使用展开运算符 ... 来创建新对象。

使用 useMemo 和 useCallback 优化

useMemo

useMemo 是 React 提供的一个 Hook,用于在函数组件中缓存计算结果。它接收两个参数:一个是需要执行的函数,另一个是依赖数组。只有当依赖数组中的值发生变化时,useMemo 才会重新计算并返回新的值,否则会返回缓存的结果。

在应用中,如果有一些复杂的计算逻辑,每次组件渲染都执行这些计算会消耗大量性能。使用 useMemo 可以将这些计算结果缓存起来,避免不必要的重复计算。

代码示例

import React, { useMemo } from'react';

const expensiveCalculation = (a, b) => {
  console.log('执行复杂计算');
  // 模拟复杂计算
  for (let i = 0; i < 10000000; i++) {
    // 空循环,增加计算时间
  }
  return a + b;
};

const MemoizedCalculation = () => {
  const [num1, setNum1] = React.useState(1);
  const [num2, setNum2] = React.useState(2);

  const result = useMemo(() => expensiveCalculation(num1, num2), [num1, num2]);

  return (
    <div>
      <input
        type="number"
        value={num1}
        onChange={(e) => setNum1(parseInt(e.target.value, 10))}
      />
      <input
        type="number"
        value={num2}
        onChange={(e) => setNum2(parseInt(e.target.value, 10))}
      />
      <p>计算结果: {result}</p>
    </div>
  );
};

export default MemoizedCalculation;

在上述代码中,expensiveCalculation 函数模拟了一个复杂的计算。useMemo 缓存了这个计算结果,只有当 num1num2 发生变化时,才会重新执行 expensiveCalculation 函数。如果只是组件的其他部分发生变化(比如组件的状态与 num1num2 无关的更新),result 会使用缓存的值,从而提升性能。

useCallback

useCallbackuseMemo 类似,也是用于缓存。不过,useCallback 缓存的是函数,而不是计算结果。它接收两个参数:一个是需要缓存的函数,另一个是依赖数组。只有当依赖数组中的值发生变化时,useCallback 才会返回新的函数,否则会返回缓存的函数。

在 React 应用中,经常会在组件内部定义函数并传递给子组件。如果这些函数没有使用 useCallback 进行缓存,每次父组件渲染时,都会创建新的函数实例。这可能会导致子组件不必要的重新渲染,特别是当子组件使用 React.memo 进行优化时。

代码示例

import React, { useCallback, useState } from'react';

const ChildComponent = React.memo(({ handleClick }) => {
  console.log('ChildComponent 渲染');
  return <button onClick={handleClick}>点击我</button>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 使用 useCallback 缓存函数
  const incrementCount = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>计数: {count}</p>
      <ChildComponent handleClick={incrementCount} />
    </div>
  );
};

export default ParentComponent;

在这个例子中,ChildComponent 使用了 React.memo 进行优化。如果 ParentComponent 内部的 incrementCount 函数没有使用 useCallback 缓存,每次 ParentComponent 渲染时,都会创建一个新的 incrementCount 函数实例。由于 ChildComponent 使用 React.memoprops 进行浅比较,新的函数实例会被认为是 props 发生了变化,从而导致 ChildComponent 不必要的重新渲染。而使用 useCallback 缓存函数后,只有当 count 发生变化时,incrementCount 函数才会更新,避免了 ChildComponent 不必要的重新渲染。

虚拟列表优化长列表渲染

在 React 应用中,经常会遇到需要渲染大量数据列表的情况。如果直接渲染整个列表,随着数据量的增加,性能会急剧下降。虚拟列表是一种优化长列表渲染的有效技术,它只渲染当前可见区域的列表项,而不是渲染整个列表。

实现原理

虚拟列表的实现原理主要基于以下几点:

  1. 计算可见区域:根据列表容器的高度、列表项的高度以及当前滚动位置,计算出当前可见区域内的列表项索引范围。
  2. 只渲染可见项:只渲染可见区域内的列表项,而不是渲染整个列表。
  3. 动态更新:当滚动位置发生变化时,重新计算可见区域,并更新渲染的列表项。

代码示例

import React, { useRef, useState, useEffect } from'react';

const VirtualList = ({ data, itemHeight, containerHeight }) => {
  const listRef = useRef(null);
  const [startIndex, setStartIndex] = useState(0);
  const [endIndex, setEndIndex] = useState(0);

  useEffect(() => {
    const updateVisibleRange = () => {
      if (!listRef.current) return;
      const scrollTop = listRef.current.scrollTop;
      const visibleCount = Math.floor(containerHeight / itemHeight);
      const newStartIndex = Math.floor(scrollTop / itemHeight);
      const newEndIndex = newStartIndex + visibleCount;
      setStartIndex(newStartIndex);
      setEndIndex(newEndIndex);
    };
    updateVisibleRange();
    listRef.current.addEventListener('scroll', updateVisibleRange);
    return () => {
      listRef.current.removeEventListener('scroll', updateVisibleRange);
    };
  }, [containerHeight, itemHeight]);

  return (
    <div
      ref={listRef}
      style={{ height: containerHeight, overflowY: 'auto' }}
    >
      <div style={{ height: data.length * itemHeight }}>
        {data.slice(startIndex, endIndex).map((item, index) => (
          <div
            key={index + startIndex}
            style={{ height: itemHeight, borderBottom: '1px solid #ccc', padding: '10px' }}
          >
            {item}
          </div>
        ))}
      </div>
    </div>
  );
};

const App3 = () => {
  const largeData = Array.from({ length: 1000 }, (_, i) => `列表项 ${i + 1}`);
  return (
    <div>
      <VirtualList
        data={largeData}
        itemHeight={50}
        containerHeight={400}
      />
    </div>
  );
};

export default App3;

在上述代码中,VirtualList 组件实现了虚拟列表功能。useEffect 钩子函数在组件挂载和更新时,通过计算滚动位置和可见项数量,确定当前可见区域的列表项索引范围(startIndexendIndex)。然后,通过 data.slice(startIndex, endIndex) 只渲染可见区域内的列表项。这样,无论数据量有多大,始终只渲染当前可见的部分,大大提升了性能。

代码分割与懒加载

代码分割的概念

在 React 应用中,随着功能的增加,打包后的 JavaScript 文件体积会越来越大。这会导致应用的加载时间变长,特别是在网络环境较差的情况下。代码分割是一种将代码拆分成多个小块的技术,这样可以按需加载代码,而不是一次性加载整个应用的代码。

懒加载的实现

React 提供了 React.lazySuspense 来实现组件的懒加载,这是代码分割的一种常见应用场景。React.lazy 用于动态导入组件,只有在组件实际需要渲染时才会加载其代码。Suspense 组件用于在组件加载过程中显示加载状态。

代码示例

import React, { lazy, Suspense } from'react';

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

const App4 = () => {
  return (
    <div>
      <Suspense fallback={<div>加载中...</div>}>
        <LazyLoadedComponent />
      </Suspense>
    </div>
  );
};

export default App4;

在上述代码中,LazyLoadedComponent 使用 React.lazy 进行懒加载。React.lazy 接收一个函数,该函数返回一个动态导入的组件。Suspense 组件的 fallback 属性用于指定在组件加载过程中显示的加载提示。这样,当 App4 组件渲染时,LazyLoadedComponent 的代码不会立即加载,只有当 LazyLoadedComponent 即将渲染时,才会异步加载其代码。

代码分割和懒加载不仅可以提升应用的初始加载速度,还可以使应用更加高效地利用网络资源,特别是对于用户可能不会立即使用到的功能模块。

优化 React 应用的 CSS

减少重排和重绘

在 React 应用中,频繁的重排(reflow)和重绘(repaint)会影响性能。重排是指浏览器重新计算元素的几何属性(如位置、大小等),重绘是指浏览器重新绘制元素的外观。以下是一些减少重排和重绘的方法:

  1. 批量修改样式:不要在循环中多次修改元素的样式,而是一次性修改。例如:
import React, { useEffect } from'react';

const MyElement = () => {
  const elementRef = useRef(null);
  useEffect(() => {
    const element = elementRef.current;
    if (element) {
      // 批量修改样式
      element.style.cssText = 'width: 100px; height: 100px; background-color: red;';
    }
  }, []);
  return <div ref={elementRef}></div>;
};

export default MyElement;
  1. 避免频繁访问会触发重排的属性:一些属性,如 offsetTopclientWidth 等,访问它们会触发重排。尽量减少对这些属性的频繁访问。例如:
import React, { useEffect } from'react';

const AnotherElement = () => {
  const elementRef = useRef(null);
  useEffect(() => {
    const element = elementRef.current;
    if (element) {
      // 避免频繁访问触发重排的属性
      const width = element.clientWidth;
      // 这里可以对 width 进行操作,而不是多次访问 clientWidth
    }
  }, []);
  return <div ref={elementRef}></div>;
};

export default AnotherElement;

使用 CSS 动画和过渡

在 React 应用中,合理使用 CSS 动画和过渡可以提升用户体验,同时也可以优化性能。与 JavaScript 动画相比,CSS 动画和过渡通常更高效,因为它们由浏览器的合成线程处理,而不是主线程。

例如,使用 CSS 过渡实现淡入效果:

.fade-in {
  opacity: 0;
  transition: opacity 0.5s ease-in;
}

.fade-in.active {
  opacity: 1;
}
import React, { useState, useEffect } from'react';
import './styles.css';

const FadeInComponent = () => {
  const [isVisible, setIsVisible] = useState(false);
  useEffect(() => {
    setIsVisible(true);
  }, []);
  return (
    <div className={`fade-in ${isVisible? 'active' : ''}`}>
      淡入内容
    </div>
  );
};

export default FadeInComponent;

在上述代码中,通过 CSS 过渡实现了淡入效果,这种方式比使用 JavaScript 动画更高效,因为它利用了浏览器的合成线程来处理动画,减少了主线程的负担,从而提升了应用的性能。

优化 React 应用的图片加载

图片压缩

在 React 应用中,图片通常占据较大的体积,是影响加载速度的重要因素之一。对图片进行压缩可以显著减少图片文件的大小,从而加快图片的加载速度。可以使用工具如 ImageOptim、Compressor.io 等对图片进行压缩。在开发过程中,也可以使用构建工具(如 Webpack)的插件来自动压缩图片。

响应式图片

为不同设备和屏幕尺寸提供合适尺寸的图片也是优化图片加载的重要方法。可以使用 HTML 的 srcsetsizes 属性来实现响应式图片。在 React 中,可以这样使用:

import React from'react';

const ResponsiveImage = () => {
  return (
    <img
      src="small.jpg"
      srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 2000w"
      sizes="(max-width: 500px) 100vw, (max-width: 1000px) 50vw, 33vw"
      alt="响应式图片"
    />
  );
};

export default ResponsiveImage;

在上述代码中,srcset 属性指定了不同尺寸的图片及其对应的像素密度描述符(500w1000w2000w),sizes 属性根据屏幕宽度定义了图片在不同屏幕尺寸下的显示宽度。浏览器会根据设备的屏幕分辨率和可用宽度,自动选择最合适的图片进行加载,从而避免加载过大尺寸的图片,提升加载速度。

图片懒加载

图片懒加载是指图片在即将进入浏览器视口时才进行加载,而不是在页面加载时就全部加载。在 React 中,可以使用第三方库如 react - lazyload 来实现图片懒加载。

安装 react - lazyload

npm install react - lazyload

使用示例:

import React from'react';
import LazyLoad from'react - lazyload';

const LazyLoadedImage = () => {
  return (
    <LazyLoad>
      <img src="image.jpg" alt="懒加载图片" />
    </LazyLoad>
  );
};

export default LazyLoadedImage;

在上述代码中,react - lazyload 会在图片即将进入视口时触发图片的加载,这样可以避免提前加载用户可能看不到的图片,节省网络资源,提升应用的整体加载速度。

优化 React 应用的网络请求

合并网络请求

在 React 应用中,如果有多个网络请求同时发出,可能会导致网络拥塞,影响加载速度。可以将一些相关的网络请求合并为一个请求,减少请求次数。例如,如果需要从服务器获取多个相关的数据,可以在服务器端提供一个接口来返回这些数据,而不是分别请求多个接口。

使用缓存

合理使用缓存可以减少网络请求次数,从而提升应用性能。在 React 应用中,可以使用浏览器的本地存储(localStorage)或会话存储(sessionStorage)来缓存一些不经常变化的数据。例如:

import React, { useEffect } from'react';

const DataComponent = () => {
  const [data, setData] = useState(null);
  useEffect(() => {
    const cachedData = localStorage.getItem('myData');
    if (cachedData) {
      setData(JSON.parse(cachedData));
    } else {
      // 发起网络请求获取数据
      fetch('api/data')
      .then(response => response.json())
      .then(data => {
          setData(data);
          localStorage.setItem('myData', JSON.stringify(data));
        });
    }
  }, []);
  return (
    <div>
      {data && <p>{JSON.stringify(data)}</p>}
    </div>
  );
};

export default DataComponent;

在上述代码中,首先尝试从 localStorage 中读取缓存数据。如果缓存数据存在,则直接使用;如果不存在,则发起网络请求获取数据,并在获取成功后将数据存入 localStorage,以便下次使用。这样可以在一定程度上减少网络请求次数,提升应用的加载速度。

优化请求时机

在 React 应用中,合理控制网络请求的时机也很重要。例如,在组件挂载时,如果某些数据不是立即需要的,可以延迟请求。另外,如果组件频繁更新导致网络请求频繁触发,可以使用防抖(debounce)或节流(throttle)技术来控制请求频率。

使用防抖函数示例:

import React, { useState, useEffect } from'react';

const debounce = (func, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
};

const SearchComponent = () => {
  const [query, setQuery] = useState('');
  const [searchResults, setSearchResults] = useState(null);

  const fetchSearchResults = debounce((q) => {
    // 发起搜索请求
    fetch(`api/search?q=${q}`)
    .then(response => response.json())
    .then(data => setSearchResults(data));
  }, 500);

  useEffect(() => {
    if (query) {
      fetchSearchResults(query);
    } else {
      setSearchResults(null);
    }
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      {searchResults && <p>{JSON.stringify(searchResults)}</p>}
    </div>
  );
};

export default SearchComponent;

在上述代码中,debounce 函数实现了防抖功能。在 SearchComponent 中,当用户输入搜索关键词时,fetchSearchResults 函数会被调用,但不会立即发起网络请求,而是在用户停止输入 500 毫秒后才发起请求,避免了频繁触发请求,提升了性能。

通过以上多种优化技巧的综合应用,可以显著提升 React 应用的加载速度,为用户提供更好的体验。在实际开发中,需要根据应用的具体情况,选择合适的优化方法,并不断进行性能测试和调优。