前端性能优化,是每个前端工程师的必修课。

我最近接手了一个项目,性能很差:首屏加载要5秒,页面切换卡顿,列表滚动掉帧。用户抱怨不断,老板要求优化。我花了两周时间,把性能从"不能用"优化到"很流畅"。

本文通过这个真实项目的性能优化案例,对比React和Vue两个框架的优化方法,从加载性能、运行时性能、渲染性能、网络性能等多个维度,分享从慢到快的完整优化过程。

一、项目背景和问题

先说说项目背景。

1. 项目情况

这是一个中后台管理系统,功能包括:

  • 数据看板:各种图表和统计
  • 列表管理:数据表格,支持筛选、排序、分页
  • 表单管理:复杂的表单,支持动态字段
  • 用户管理:权限、角色、用户管理

技术栈:

  • 前端:React 17 + Ant Design 4
  • 后端:Node.js + MySQL
  • 构建:Webpack 4

项目已经运行了两年,代码量越来越大,性能越来越差。

2. 性能问题

主要的性能问题:

  • 首屏加载慢:5秒以上,白屏时间长
  • 页面切换卡顿:切换页面要1-2秒
  • 列表滚动掉帧:大数据量列表滚动不流畅
  • 表单响应慢:输入框输入有延迟
  • 打包体积大:主包超过2MB

3. 优化目标

我定的优化目标:

  • 首屏加载 < 2秒
  • 页面切换 < 300ms
  • 列表滚动 60fps
  • 输入响应 < 16ms
  • 主包体积 < 500KB

二、性能分析

优化之前,先做性能分析,找到瓶颈。

1. 性能分析工具

我用了这些工具:

  • Lighthouse:整体性能评分和建议
  • Chrome DevTools:Performance面板分析运行时性能,Network面板分析加载
  • Webpack Bundle Analyzer:分析打包体积
  • React DevTools Profiler:分析React组件渲染性能

2. 分析结果

分析后发现的主要问题:

加载性能:

  • 打包体积大:主包2.3MB,gzip后800KB
  • 没有代码分割:所有页面都打在一个包里
  • 第三方库太大:moment.js、lodash全量引入
  • 没有缓存:静态资源没有合理的缓存策略

运行时性能:

  • 不必要的重渲染:父组件状态变化,所有子组件都重新渲染
  • 列表没有虚拟化:1000条数据一次性渲染
  • 表单没有优化:每次输入都触发整个表单重渲染
  • 计算属性没有缓存:每次渲染都重新计算

网络性能:

  • 接口请求多:一个页面发十几个请求
  • 没有请求合并:相关的接口分开请求
  • 没有缓存:相同的数据重复请求
  • 图片太大:没有压缩和懒加载

三、加载性能优化

先优化加载性能。

1. 代码分割

这是最有效的优化手段。

优化前: 所有页面都打在一个包里,主包2.3MB。

优化后: 用React.lazy和Suspense做路由级别的代码分割,每个页面单独打包。

// 优化前
import Dashboard from './pages/Dashboard';
import UserList from './pages/UserList';

// 优化后
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const UserList = React.lazy(() => import('./pages/UserList'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/users" element={<UserList />} />
      </Routes>
    </Suspense>
  );
}

效果: 主包从2.3MB降到400KB,首屏只加载需要的页面代码。

Vue中的对应方案: Vue用异步组件实现代码分割:

const Dashboard = () => import('./pages/Dashboard.vue');

2. 第三方库优化

优化前: moment.js全量引入(含所有语言包),lodash全量引入。

优化后:

  • moment.js换成day.js(体积小很多,API兼容)
  • lodash按需引入,只引入用到的函数
  • Ant Design按需引入,只引入用到的组件
// 优化前
import moment from 'moment';
import _ from 'lodash';
import { Button, Input, Table } from 'antd';

// 优化后
import dayjs from 'dayjs';
import debounce from 'lodash/debounce';
import Button from 'antd/es/button';

效果: 第三方库体积减少了60%。

3. 资源压缩

  • 开启gzip压缩(服务端配置)
  • 图片压缩,用WebP格式
  • 字体文件子集化,只包含用到的字符
  • 移除未使用的CSS(PurgeCSS)

4. 缓存策略

  • 静态资源设置长期缓存(Cache-Control: max-age=31536000)
  • 文件名加hash,内容变化时文件名变化
  • HTML不缓存,确保能获取最新版本
  • 接口数据缓存,避免重复请求

5. 预加载和预获取

  • 关键资源用preload提前加载
  • 下一个页面可能用到的资源用prefetch预获取
  • 首屏图片优先加载,非首屏图片懒加载

四、运行时性能优化

加载快了,运行时也要快。

1. 避免不必要的重渲染

这是React性能优化的重点。

优化前: 父组件状态变化,所有子组件都重新渲染,即使props没变。

优化后:

  • 用React.memo包裹纯组件,props不变就不重新渲染
  • 用useMemo缓存计算结果
  • 用useCallback缓存函数引用,避免子组件因为函数引用变化而重渲染
// 优化前
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => setCount(c => c + 1);
  const data = computeData(); // 每次渲染都重新计算
  return <Child data={data} onClick={handleClick} />;
}

// 优化后
const Child = React.memo(function Child({ data, onClick }) {
  return <div onClick={onClick}>{data}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount(c => c + 1), []);
  const data = useMemo(() => computeData(), []);
  return <Child data={data} onClick={handleClick} />;
}

Vue中的对应方案:

  • 用v-once渲染不需要更新的内容
  • 用v-memo缓存组件
  • 计算属性(computed)自动缓存
  • 函数式组件(Vue 3中默认优化)

2. 列表虚拟化

优化前: 1000条数据一次性渲染,DOM节点多,滚动卡顿。

优化后: 用react-window做列表虚拟化,只渲染可见区域的项。

import { FixedSizeList as List } from 'react-window';

function VirtualList({ data }) {
  const Row = ({ index, style }) => (
    <div style={style}>{data[index].name}</div>
  );

  return (
    <List
      height={500}
      itemCount={data.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </List>
  );
}

效果: 1000条列表,DOM节点从1000个降到20个,滚动流畅。

Vue中的对应方案: 用vue-virtual-scroller。

3. 表单优化

优化前: 表单有50个字段,每次输入都触发整个表单重渲染,输入有延迟。

优化后:

  • 表单字段拆分成独立组件,每个字段自己管理状态
  • 用uncontrolled组件,减少重渲染
  • 输入防抖,避免频繁触发校验
  • 复杂表单用Formily或React Hook Form等优化过的表单库
// 优化前:整个表单一个组件,每次输入都重渲染所有字段
function BigForm() {
  const [form, setForm] = useState({});
  const handleChange = (key, value) => setForm({ ...form, [key]: value });
  return (
    <div>
      <Input value={form.name} onChange={e => handleChange('name', e.target.value)} />
      <Input value={form.email} onChange={e => handleChange('email', e.target.value)} />
      {/* 50个字段 */}
    </div>
  );
}

// 优化后:每个字段独立组件,自己管理状态
function Field({ label, value, onChange }) {
  const [localValue, setLocalValue] = useState(value);
  const handleChange = e => {
    setLocalValue(e.target.value);
    onChange(e.target.value);
  };
  return (
    <div>
      <label>{label}</label>
      <Input value={localValue} onChange={handleChange} />
    </div>
  );
}

4. 大数据量处理

  • 大数据量的计算用Web Worker,不阻塞主线程
  • 复杂计算分批执行,用requestIdleCallback
  • 数据分页,不要一次性加载所有数据
  • 用虚拟滚动,只渲染可见数据

五、渲染性能优化

1. 避免布局抖动

  • 批量读取和写入DOM,不要交替读写
  • 用transform和opacity做动画,不触发重排
  • 避免在滚动事件中修改样式
  • 用will-change提示浏览器优化

2. 图片优化

  • 图片尺寸和显示尺寸一致
  • 用WebP格式,体积更小
  • 懒加载,非首屏图片不加载
  • 用占位图,避免布局抖动

3. CSS优化

  • 避免使用昂贵的CSS属性(box-shadow、filter等)
  • 减少选择器的复杂度
  • 避免使用@import
  • 用CSS containment隔离渲染区域

六、网络性能优化

1. 接口优化

  • 合并请求:相关的接口合并成一个
  • 接口聚合:后端提供BFF层,聚合多个接口
  • 减少不必要的字段:接口只返回需要的字段
  • 分页加载:大数据量分页,不要一次性返回

2. 请求缓存

  • 相同的请求做缓存,避免重复请求
  • 用SWR或React Query管理请求缓存
  • 接口数据预加载,进入页面时数据已经准备好了

3. HTTP优化

  • 用HTTP/2,多路复用
  • 启用gzip/brotli压缩
  • 用CDN加速静态资源
  • DNS预解析,减少DNS查询时间

七、React vs Vue 优化对比

说说React和Vue在性能优化上的差异。

1. 渲染优化

React:

  • 需要手动优化:React.memo、useMemo、useCallback
  • 灵活性高,但需要开发者有优化意识
  • 不当的写法容易导致性能问题
  • React 18的并发模式,提供了更多优化手段

Vue:

  • 自动优化:编译时优化,响应式系统精确更新
  • 不需要手动memo,默认就比较快
  • 对开发者更友好,不容易写出性能差的代码
  • Vue 3的Proxy响应式,性能更好

总结: Vue默认性能更好,React需要手动优化但上限更高。

2. 代码分割

两者都支持,方式类似:

  • React:React.lazy + Suspense
  • Vue:异步组件 + Suspense(Vue 3)

3. 列表虚拟化

都需要第三方库:

  • React:react-window、react-virtualized
  • Vue:vue-virtual-scroller

4. 状态管理

React:

  • Redux、Zustand、Jotai等
  • 需要注意选择器,避免不必要的重渲染
  • 状态管理的性能取决于使用方式

Vue:

  • Pinia、Vuex
  • 响应式系统自动优化,不需要手动处理
  • 默认性能就很好

5. 构建优化

两者都用Vite或Webpack,优化方式类似:

  • 代码分割
  • 按需引入
  • 压缩和缓存
  • Tree Shaking

八、优化效果

说说优化后的效果。

1. 加载性能

  • 首屏加载:从5秒降到1.5秒,提升70%
  • 主包体积:从2.3MB降到400KB,减少83%
  • 白屏时间:从3秒降到0.5秒

2. 运行时性能

  • 页面切换:从1.5秒降到200ms,提升87%
  • 列表滚动:从30fps升到60fps
  • 输入响应:从100ms降到10ms以内

3. 用户体验

  • 用户抱怨明显减少
  • 页面操作流畅,没有卡顿感
  • 数据看板加载快,交互响应及时

4. Lighthouse评分

  • 性能:从30分升到90分
  • 可访问性:从70分升到95分
  • 最佳实践:从80分升到95分

九、性能优化的一般思路

总结一下性能优化的一般思路。

1. 先测量,再优化

不要盲目优化,先用工具测量,找到瓶颈。

  • Lighthouse看整体评分
  • DevTools看具体问题
  • Bundle Analyzer看体积
  • Profiler看渲染性能

2. 优先优化影响最大的

  • 20%的问题导致80%的性能问题
  • 先优化影响最大的瓶颈
  • 不要在细枝末节上浪费时间

3. 加载性能和运行时性能都要管

  • 加载性能:用户多久能看到页面
  • 运行时性能:用户操作是否流畅
  • 两者都重要,不能只优化一个

4. 建立性能监控

  • 上线后持续监控性能
  • 建立性能预算,防止性能退化
  • 每次发布都跑性能测试
  • 用真实用户数据(RUM)监控

5. 不要过度优化

  • 优化是有成本的,会增加代码复杂度
  • 只优化真正的瓶颈
  • 优化后要测试,确保没有引入bug
  • 可读性和可维护性也很重要

十、写在最后

这次性能优化,让我对前端性能有了更深的理解。

性能优化不是一蹴而就的,需要持续的关注和投入。从加载到运行时,从渲染到网络,每个环节都有优化的空间。

React和Vue,各有各的优化方式。React需要手动优化,但灵活性高;Vue自动优化,对开发者更友好。但无论用什么框架,性能优化的核心思路是一样的:找到瓶颈,针对性优化,持续监控。

2022年了,前端性能越来越受到重视。用户对性能的期望越来越高,慢一点就会流失用户。作为前端工程师,我们要把性能优化当成日常工作的一部分,而不是出了问题才去救火。

最后,用一句话总结:"性能优化没有银弹,测量、定位、优化、验证,一步一步来。加载要快,运行要流畅,网络要高效,用户体验才会好。"

愿你的项目,又快又稳。