前端性能优化实战:一个后台系统的真实优化清单
从加载、渲染到运行时,一份能直接落地的性能优化清单。每个建议背后,都是我在 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,缓存了就会显示旧数据。
老实说,最值钱的是这几个坑
willChange用了忘清,显存一直占着,页面越来越卡。transitionend 里清。- Observer 和事件监听没在卸载时清理,页面切走回调还在跑,控制台一堆僵尸请求。
- 想优化之前先量化。我一开始也凭感觉瞎调,后来老老实实开 Performance 面板看火焰图,确认是渲染慢还是请求慢再动手,效率完全不一样。
优化没有银弹,一次改一处,改完量一下前后对比。后台系统的性能优化其实不炫技,就是让每天坐你旁边的同事少等那几百毫秒。