前端性能优化实战:一个后台系统的真实优化清单

从加载、渲染到运行时,一份能直接落地的性能优化清单。每个建议背后,都是我在 Vue3 + Ant Design 后台系统里真实做过、验证过的改动。

前端性能优化实战:一个后台系统的真实优化清单

先说结论:性能优化不是玄学,是能一条条打勾的活。这篇讲的是我在一个 Vue3 + Ant Design 后台系统里真动过手的东西,代码都是从项目里抠出来的,不是网上抄的模板。

先搞清楚优化谁

Core Web Vitals 那四个指标大家都熟,但我做后台系统有个体感:INP 比 LCP 重要

为什么?C 端官网拼首屏速度,用户看一眼就走;后台是用户天天用的工具,他们最痛的是”点一下卡半天”和”表格滚着滚着就掉帧”。所以下面我大部分篇幅在讲交互和渲染,不是首屏加载。

加载:少塞点东西

图片 WebP + lazy、路由懒加载、CDN,这些是标配,不展开。

讲一个容易踩的坑。后台系统里”管控对象多选”这个功能,用户是真能给你选几百个标签的。一开始全选直接塞进请求,接口慢不说,表单上渲染几百个 tag 直接卡住。后来我在 change 事件里截断:

const handleControlMapChange = values => {
  if (values?.length > 200) {
    formState.value.control_map = values.slice(0, 200);
    return message.error('已达200个标签选择上限');
  }
};

这种”前端先限流”的做法很土,但特别有效。比啥算法优化都实在。

渲染:别让浏览器反复干活

高频事件用 throttle

我的引导组件要跟着页面元素定位,resize 的时候疯狂重算位置,肉眼可见地抖。加了 throttle 就好很多:

this.resizeHandler = throttle(() => this.updatePosition(), 300);
this.observer = new MutationObserver(() => this.updatePosition());

这里用 throttle 不用 debounce,因为引导要”持续跟手”,不是”停下来了再动”。

动画走合成层

抽屉展开动画,一开始直接改 height,掉帧明显。改成这样:

const start = el.scrollHeight;
setHeight('0px');
el.style.willChange = 'height';
requestAnimationFrame(() => setHeight(`${start}px`));

willChange 提前告诉浏览器”这个要动画了”,让它走合成层。但记得在 transitionend 里把 willChange 清掉——这个我忘过,显存一直被占着,页面越用越卡。

滚到才处理

工单消息列表,每条消息进去就要标已读。一开始进页面就全标,滚动一下发几十个请求。后来改成 IntersectionObserver,滚到哪条标哪条:

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (!entry.isIntersecting) return;
    const target = recordList.value[entry.target.dataset.index];
    if (!target || target.is_read) return;
    markMessageRead(target.id);
  });
});

onBeforeUnmount(() => observer?.disconnect());

最后一行别漏。Observer 不 disconnect 的话,页面切走了回调还在跑,内存泄漏。

运行时:别让请求打架

批量请求

还是消息已读。用 IntersectionObserver 之后,滚动快的话一条条标,请求还是多。干脆收集起来,3 秒防抖合并一次提交:

const list: string[] = [];
const executeRequest = debounce(() => {
  if (!list.length) return;
  postReadWorkOrderReply({ id: list.join(',') }).then(() => { list.length = 0; });
}, 3000);

function markMessageRead(id: string) {
  if (!list.includes(id)) list.push(id);
  executeRequest();
}

性能优化的终极形态不是让单个请求变快,是让请求变少。这个思路后端要配合,支持批量参数,但收益是网络往返 N 次变 1 次。

loading 防重复提交

用户手滑连点两次提交按钮,同一个接口打两遍,数据库还可能重复写。用个 loading 挡一下:

const [submitLoading, setSubmitLoading] = useBoolean();
async function onSubmit() {
  if (submitLoading.value) return;
  setSubmitLoading(true);
  try { await api.submit(); } finally { setSubmitLoading(false); }
}

Vue 的响应式别过度

大列表大表单里有些数据只读不写,用 ref 包起来纯属浪费——深层全给你代理了。改用 shallowRef 只包一层:

const hitInfo = shallowRef();   // 只读的大对象,浅引用

页面缓存

报表页在标签间切来切去,每次切都重新拉接口。包个 keep-alive,切回去就不重新请求了:

<keep-alive>
  <a-spin v-if="activeKey === '1'" :spinning="loading">…总览…</a-spin>
  <CategoryList v-else />
</keep-alive>

但注意,数据实时性高的页面别用 keep-alive,缓存了就会显示旧数据。

老实说,最值钱的是这几个坑

  1. willChange 用了忘清,显存一直占着,页面越来越卡。transitionend 里清。
  2. Observer 和事件监听没在卸载时清理,页面切走回调还在跑,控制台一堆僵尸请求。
  3. 想优化之前先量化。我一开始也凭感觉瞎调,后来老老实实开 Performance 面板看火焰图,确认是渲染慢还是请求慢再动手,效率完全不一样。

优化没有银弹,一次改一处,改完量一下前后对比。后台系统的性能优化其实不炫技,就是让每天坐你旁边的同事少等那几百毫秒。