加载中...

常规的React项目(优化一)


在优化 React 应用时,有许多具体的细节可以关注,以提升性能、可维护性和用户体验。以下是一些关键的优化细节

1. 使用React.memo

对于纯组件(Pure Components),使用 React.memo 包装 防止不必要的重复渲染

示例:

const myComponent = React.memo((props) => {
  
});

2. 使用 useCallback 和 useMemo

useCallback 用于缓存函数引用,避免子组件因为传入函数变化而重渲染。

useMemo用于缓存计算结果,避免每次渲染时都重新计算。

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

const memovizedValue = useMemo(()=> computeExpensiveValue(a,b)), [a, b];

3. 拆分组件

将大组件拆分更小的可复用组件,提高代码可读性和性能(通过减少必要的重渲染)

场景说明:
一个简单的任务列表组件,允许用户添加任务和展示任务。如果任务列表和输入框逻辑都写在一个组件中,代码会显得臃肿,且每次状态更新时,整个组件都会重新渲染。通过拆分组件,我们可以优化其性能和结构。

3.1 未优化的代码(未拆分的大组件)

import React, { useState } from "react";

const TaskApp = () => {
  const [tasks, setTasks] = useState<string[]>([]);
  const [newTask, setNewTask] = useState("");

  const addTask = () => {
    if (newTask.trim()) {
      setTasks([...tasks, newTask]);
      setNewTask("");
    }
  };

  return (
    <div>
      <h1>任务列表</h1>
      <input
        type="text"
        value={newTask}
        onChange={(e) => setNewTask(e.target.value)}
      />
      <button onClick={addTask}>添加任务</button>
      <ul>
        {tasks.map((task, index) => (
          <li key={index}>{task}</li>
        ))}
      </ul>
    </div>
  );
};

export default TaskApp;

问题:

  1. 所有逻辑都集中在一个组件中,可读性和维护性差。
  2. 添加任务时,整个组件重新渲染,包括任务列表。

3.2优化后的代码(拆分组件)

优化结构,拆分为更小的组件

import React, { useState, useCallback, memo } from "react";

// 子组件:任务输入框
const TaskInput = memo(({ onAddTask }: { onAddTask: (task: string) => void }) => {
  const [inputValue, setInputValue] = useState("");

  const handleAddTask = () => {
    if (inputValue.trim()) {
      onAddTask(inputValue);
      setInputValue("");
    }
  };

  console.log("TaskInput 渲染");

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleAddTask}>添加任务</button>
    </div>
  );
});

// 子组件:任务列表
const TaskList = memo(({ tasks }: { tasks: string[] }) => {
  console.log("TaskList 渲染");

  return (
    <ul>
      {tasks.map((task, index) => (
        <li key={index}>{task}</li>
      ))}
    </ul>
  );
});

// 主组件:任务应用
const TaskApp = () => {
  const [tasks, setTasks] = useState<string[]>([]);

  // 使用 useCallback 优化回调函数,避免每次创建新函数
  const addTask = useCallback(
    (task: string) => {
      setTasks((prevTasks) => [...prevTasks, task]);
    },
    [] // 依赖为空数组,表示回调函数不会因外部状态变化而重新创建
  );

  return (
    <div>
      <h1>任务列表</h1>
      <TaskInput onAddTask={addTask} />
      <TaskList tasks={tasks} />
    </div>
  );
};

export default TaskApp;

优化后的特点

组件拆分:

TaskInput:负责处理用户输入和任务添加。

TaskList:负责展示任务列表。

这样每个组件职责单一,易于维护和测试。

性能优化:

使用 React.memo 防止子组件在不必要的情况下重新渲染。

使用 useCallback 保证传递给子组件的 onAddTask 回调函数不会每次重新创建,减少子组件渲染。

减少重渲染:

添加任务时,仅 TaskListTaskInput 中必要的部分会更新,而不会重新渲染整个组件树。

4. 避免匿名函数和对象

在 JSX 中避免使用匿名函数和对象,因为每次渲染都会创建新的实例。

// 不推荐
<MyComponent onClick={()=> handleClick()} style={{color: 'blue'}}></MyComponent>

// 推荐
const handleClick = useCallback(()=> {
  // 逻辑处理
}, []);
const style={color: 'blue'}
<MyComponent onClick={handleClick()} style={style}></MyComponent>

场景说明:
在 React 中,JSX 中使用匿名函数或对象会导致性能问题,因为每次组件渲染时,匿名函数或对象会被重新创建,导致传递给子组件的属性(props)总是新实例,从而触发子组件的重渲染,即使它的 props 内容没有变化。

4.1 未优化代码:在 JSX 中使用匿名函数和对象

import React, { useState } from "react";

const ChildComponent = ({ onClick, style }: { onClick: () => void; style: React.CSSProperties }) => {
  console.log("ChildComponent 渲染");
  return (
    <button onClick={onClick} style={style}>
      点击我
    </button>
  );
};

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

  return (
    <div>
      <h1>计数器:{count}</h1>
      <ChildComponent
        onClick={() => setCount(count + 1)} // 每次渲染都创建新的函数实例
        style={{ color: "blue", fontSize: "16px" }} // 每次渲染都创建新的对象实例
      />
    </div>
  );
};

export default ParentComponent;
问题分析:
  1. 匿名函数
    • onClick={() => setCount(count + 1)} 是一个新的函数实例,每次 ParentComponent 渲染时都会重新创建。
    • 即使子组件内部逻辑和行为没有变化,React 仍然会认为 onClick 是新的,导致 ChildComponent 重渲染。
  2. 对象实例
    • style={{ color: "blue", fontSize: "16px" }} 是一个新的对象,每次渲染都会重新创建一个新对象。
    • 即使样式未变化,ChildComponent 也会重新渲染。

4.2 优化代码:避免在 JSX 中使用匿名函数和对象

使用 useCallbackuseMemo

import React, { useState, useCallback, useMemo } from "react";

const ChildComponent = ({ onClick, style }: { onClick: () => void; style: React.CSSProperties }) => {
  console.log("ChildComponent 渲染");
  return (
    <button onClick={onClick} style={style}>
      点击我
    </button>
  );
};

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

  // 使用 useCallback 记住函数实例
  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  // 使用 useMemo 记住对象实例
  const buttonStyle = useMemo(() => {
    return { color: "blue", fontSize: "16px" };
  }, []);

  return (
    <div>
      <h1>计数器:{count}</h1>
      <ChildComponent onClick={handleClick} style={buttonStyle} />
    </div>
  );
};

export default ParentComponent;
优化后的效果
  1. 使用 useCallback
    • handleClick 是固定的函数实例,不会因 ParentComponent 的重新渲染而变化。
    • React 的 memo 会根据 onClick 的引用判断是否需要重新渲染 ChildComponent
  2. 使用 useMemo
    • buttonStyle 是固定的对象实例,避免了每次重新渲染时创建新对象。
    • React 的 memo 会根据 style 的引用判断是否需要重新渲染 ChildComponent
  3. 性能提升:
    • 控制台中可以看到,只有在 count 更新时 ChildComponentonClickstyle 改变时,才会触发重新渲染。

5. 代码分割和懒加载

通过 React.lazySuspense 实现动态加载组件,可以有效减少初始加载时间,提高页面的首屏性能。代码分割的核心思想是将大文件拆分为多个小文件,按需加载,从而避免一次性加载过多内容。

const OtherComponent = React.lazy(()=>import('./OtherComponent'));
function MyComponent () {
  return (
    <Suspense fallback={<div>Loading</div>}>
      <OtherComponent/></OtherComponent>
    </Suspense>
  )
}

具体的场景 看这个说明:常规的React项目(优化四)

6. 避免不必要的副作用

确保 useEffect 中的依赖数组正确配置,避免不必要的副作用执行。

useEffect(()=> {
  // 副作用逻辑
}, [deps])

具体的场景 看这个例子: 常规的React项目(优化三)

7. 提升图片和资源加载

使用现代图片格式(如 WebP)和图像懒加载技术(如 loading=”lazy”)

<img src="image.webp" alt="example" loading="lazy" />

8. 网络请求优化

合理使用缓存,避免重复请求

// 通过缓存机制,存储已经发出的请求结果,如果同样的请求再次发起,
// 直接从缓存中获取数据,而不是重新发请求。

import axios from "axios";

// 缓存对象
const cache = new Map<string, any>();

// 封装带缓存的请求函数
async function axiosWithCache(url: string): Promise<any> {
  if (cache.has(url)) {
    console.log(`从缓存中获取数据: ${url}`);
    return cache.get(url);
  }

  console.log(`发送网络请求: ${url}`);
  const response = await axios.get(url);

  // 将结果存入缓存
  cache.set(url, response.data);

  return response.data;
}

// 使用示例
(async () => {
  const url = "https://jsonplaceholder.typicode.com/todos/1";
  const result1 = await axiosWithCache(url); // 第一次请求
  console.log(result1);

  const result2 = await axiosWithCache(url); // 从缓存获取
  console.log(result2);
})();


使用 abortController 取消不再需要的请求。

// 当用户离开页面或切换视图时,取消掉未完成的请求以节省资源。
// 封装请求函数,支持 AbortController
import axios from "axios";

// 封装带取消功能的请求函数
async function axiosWithAbort(url: string, controller: AbortController): Promise<any> {
  try {
    const response = await axios.get(url, {
      signal: controller.signal, // 绑定 AbortController 的 signal
    });
    return response.data;
  } catch (error: any) {
    if (axios.isCancel(error)) {
      console.log(`请求已取消: ${url}`);
    } else {
      console.error("请求失败", error);
    }
    throw error;
  }
}

// 使用示例
(async () => {
  const controller = new AbortController();
  const url = "https://jsonplaceholder.typicode.com/todos/1";

  // 模拟请求
  const fetchPromise = axiosWithAbort(url, controller);

  // 模拟用户取消请求
  setTimeout(() => {
    controller.abort(); // 取消请求
  }, 100);

  try {
    const result = await fetchPromise;
    console.log(result);
  } catch (error) {
    console.log("请求未完成,已被取消");
  }
})();

9. 优化 CSS 和样式

使用 CSS-in-JS 库(如 styled-components 或 Emotion)提高样式管理的灵活性和性能。

避免全局样式污染,使用模块化样式。


文章作者: LYF
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LYF !
评论
  目录