Skip to content

框架层面优化

一、Vue 层面的优化

1. 组件懒加载 + 路由懒加载

  • Vue Router 提供 defineAsyncComponent 和 () => import():
js
// 路由懒加载
const routes = [
  {
    path: '/about',
    component: () => import('@/views/About.vue'),
  },
];

// 组件懒加载
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent(() => import('./BigComp.vue'));

👉 减少首屏 bundle 体积,提升加载速度。

2. KeepAlive 缓存

  • 对切换频繁的页面/组件,使用 <KeepAlive> 保持状态和 DOM,减少重复渲染:
vue
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>

3. Vue 响应式优化

  • 使用 shallowRef、shallowReactive 避免深层监听。
  • 大列表用 v-memo(Vue3.3+)缓存虚拟节点,避免不必要 diff。
js
<div v-for="item in list" :key="item.id" v-memo="[item.id]">
  {{ item.text }}
</div>

4. SSR / SSG(Nuxt)

  • SSR 提升首屏渲染速度,SEO 更好。
  • Nuxt 3 / VitePress 等支持静态生成,适合文档/内容型项目。

React 层面的优化

1. memo

memo 允许组件在 props 没有改变的情况下跳过重新渲染

默认通过 Object.is 比较每个 prop,可通过第二个参数,传入自定义函数来控制对比过程

js
const Chart = memo(function Chart({ dataPoints }) {
  // ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
  return (
    oldProps.dataPoints.length === newProps.dataPoints.length &&
    oldProps.dataPoints.every((oldPoint, index) => {
      const newPoint = newProps.dataPoints[index];
      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
    })
  );
}

2. useMemo

在每次重新渲染的时候能够缓存计算的结果

js
import { useState, useMemo } from 'react';

function App() {
  const [count, setCount] = useState(0);

  const memoizedValue = useMemo(() => {
    //创建1000位数组
    const list = new Array(1000).fill(null).map((_, i) => i);

    //对数组求和
    const total = list.reduce((res, cur) => (res += cur), 0);

    //返回计算的结果
    return count + total;

    //添加依赖项,只有count改变时,才会重新计算
  }, [count]);

  return (
    <div>
      {memoizedValue}
      <button onClick={() => setCount((prev) => prev + 1)}>按钮</button>
    </div>
  );
}

export default App;

3. useCallback

缓存函数的引用地址,仅在依赖项改变时才会更新

js
import { useState, memo } from 'react';

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

  const handleClick = () => {
    setCount((prev) => prev + 1);
  };

  return (
    <div>
      {count}
      <MyButton handleClick={handleClick} />
    </div>
  );
};

const MyButton = memo(function MyButton({ handleClick }: { handleClick: () => void }) {
  console.log('子组件渲染');
  return <button onClick={handleClick}>按钮</button>;
});

export default App;

点击按钮,可以发现即使子组件使用 memo 包裹了,但还是更新了,控制台打印出“子组件渲染”。这是因为父组件 App 每次更新时,函数 handleClick 每次都返回了新的引用地址,因此对于子组件来说每次传入的都是不一样的值,从而触发重渲染。

同样的,减少使用通过内联函数绑定事件。每次父组件更新时,匿名函数都会返回一个新的引用地址,从而触发子组件的重渲染

js
<MyButton handleClick={() => setCount((prev) => prev + 1)} />

使用 useCallback 可以缓存函数的引用地址,将 handleClick 改为

js
const handleClick = useCallback(() => {
  setCount((prev) => prev + 1);
}, []);

再点击按钮,会发现子组件不会再重新渲染。

4. useTransition

使用 useTransition 提供的 startTransition 来标记一个更新作为不紧急的更新。这段任务可以接受延迟或被打断渲染,进而去优先考虑更重要的任务执行

页面会先显示 list2 的内容,之后再显示 list1 的内容

js
import { useState, useEffect, useTransition } from "react";

const App = () => {
  const [list1, setList1] = useState<null[]>([]);
  const [list2, setList2] = useState<null[]>([]);
  const [isPending, startTransition] = useTransition();
  useEffect(() => {
    startTransition(() => {
       //将状态更新标记为 transition
      setList1(new Array(10000).fill(null));
    });
  }, []);
  useEffect(()=>{
    setList2(new Array(10000).fill(null));
  },[])
  return (
    <>
      {isPending ? "pending" : "nopending"}
      {list1.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
      -----------------list2
      {list2.map((_, i) => (
        <div key={i}>6666</div>
      ))}
    </>
  );
};

export default App;

注释掉 list1 相关代码的执行堆栈图:

image.png

可以看到 layout 之后,一直在函数调用,页面会堵塞大约 0.3 秒

注释掉 list2,只看 list1 相关代码的执行堆栈图:

1703767814.png

可以看到我们的任务被拆分到每一帧不同的 task 中,这样浏览器就有剩余时间执行样式布局和样式绘制,减少掉帧的可能性

5. useDeferredValue

可以让我们延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间

js
import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

6. Fragment

当呈现多个元素而不需要额外的容器元素时,使用 React.Fragment 可以减少 DOM 节点的数量,从而提高呈现性能

js
const MyComponent = () => {
  return (
    <React.Fragment>
      <div>Element 1</div>
      <div>Element 2</div>
      <div>Element 3</div>
    </React.Fragment>
  );
};

7. 合理使用 Context

Context 能够在组件树间跨层级数据传递,正因其这一独特机制,Context 可以绕过 React.memo 或 shouldComponentUpdate 设定的比较过程。

也就是说,一旦 Context 的 Value 变动,所有使用 useContext 获取该 Context 的组件会全部 forceUpdate。即使该组件使用了 memo,且 Context 更新的部分 Value 与其无关

为了使组件仅在 context 与其相关的 value 发生更改时重新渲染,将组件分为两个部分。在外层组件中从 context 中读取所需内容,并将其作为 props 传递给使用 memo 优化的子组件。

8. 尽量避免使用 index 作为 key

在渲染元素列表时,尽量避免将数组索引作为组件的 key。如果列表项有添加、删除及重新排序的操作,使用 index 作为 key,可能会使节点复用率变低,进而影响性能

使用数据源的 id 作为 key

js
const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' },
  ];

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

9. 懒加载

通过 React.lazy 和 React.Suspense 实施代码分割策略,将 React 应用细分为更小的模块,确保在具体需求出现时才按需加载相应的部分

定义路由

js
import { lazy } from 'react';
import { createBrowserRouter } from 'react-router-dom';

const Login = lazy(() => import('../pages/login'));

const routes = [
  {
    path: '/login',
    element: <Login />,
  },
];

//可传第二个参数,配置base路径 { basename: "/app"}
const router = createBrowserRouter(routes);

export default router;

引用路由

js
import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';

import ReactDOM from 'react-dom/client';

import router from './router';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
  <Suspense fallback={<div>Loading...</div>}>
    <RouterProvider router={router} />
  </Suspense>,
);

10. 组件卸载时的清理

在组件卸载时清理全局监听器、定时器等。防止内存泄漏影响性能

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

function MyComponent() {
  const [count, setCount] = useState(0);
  const timer = useRef<NodeJS.Timeout>();

  useEffect(() => {
    // 定义定时器
    timer.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);

    const handleOnResize = () => {
      console.log('Window resized');
    };

    // 定义监听器
    window.addEventListener('resize', handleOnResize);

    // 在组件卸载时清除定时器和监听器
    return () => {
      clearInterval(timer.current);
      window.removeEventListener('resize', handleOnResize);
    };
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
}

export default MyComponent;

Released under the MIT License.