前端开发中那些真正管用的Best Practices实战总结
优化前:卡得不行
上周上线了个新活动页,用 Vue 3 + Vite 搭的,页面结构也不复杂:顶部 Banner、三组商品卡片、底部一个表单。结果一上线,运营同事直接微信轰炸我——“点不动”“滑两下就白屏”“安卓机点开要等 5 秒才出内容”。我自己测了下,红米 Note 10(中低端 Android)上首次加载时间 5.2s,首屏渲染 4.8s,LCP 6.1s,CLS 高到报警……不是“有点慢”,是“根本没法用”。
更离谱的是,用户还没开始操作,控制台 already 打出一堆 ResizeObserver loop completed with undelivered notifications.,连带 touchstart 都延迟 300ms+。这哪是网页,这是行为艺术。
找到病根了!
没急着改代码,先开了 Chrome DevTools 的 Performance 面板录了一把。重点看三个地方:Network(看资源加载瀑布)、Main(看 JS 执行阻塞)、Rendering(看 Layout/Recalculate Style)。果然,问题全堆在主线程:
- 首屏加载时,一口气 fetch 了 7 个接口(Banner、推荐位、热销榜、新品、分类、用户状态、埋点初始化),全是串行 await;
- 商品卡片组件里用了
v-for渲染 30+ 项,每项都绑了@click+@touchstart+class="item-{{id}}",还顺手加了个watch监听数据变化去重绘; - CSS 里写了 3 处
body * { transition: all .3s; },连<svg>都被拖进去做动画; - 最致命的是,有个「懒加载」逻辑是这样写的:
// ❌ 优化前:滚动监听器没节流,也没取消
window.addEventListener('scroll', () => {
items.forEach(item => {
if (isInViewport(item.el)) {
item.load();
}
});
});
这段代码在低端机上每秒触发 60+ 次,每次遍历 30+ DOM 节点,再调一次 getBoundingClientRect() —— 主线程直接堵死。
核心优化:砍掉 3 个“看起来很酷”的东西
我试了几种方案:Code Splitting、SSR、预加载……最后发现,根本不用那么重。真正卡顿的,就是那几个“顺手写上去”的小细节。下面这三块改完,性能直接翻倍。
1. 接口请求:从串行 await 改成并行 Promise.all + 分批
原来 7 个接口,按顺序 await fetch(...),最长那个接口超时重试两次,直接吃掉 3s。改成并发后还不够——后端返回的「热销榜」数据有 200 条,但首屏只展示前 12 条,其他纯属浪费。
最终策略:关键接口(Banner、用户状态)并行加载;非关键(热销榜、新品)只取前 12 条;埋点初始化扔到 setTimeout(..., 0) 里,不抢主线程。
// ✅ 优化后:分优先级 + 控制数据量
const [banner, user] = await Promise.all([
fetch('https://jztheme.com/api/banner'),
fetch('https://jztheme.com/api/user')
]);
// 非关键数据,且只取前12条
const hotItems = await fetch('https://jztheme.com/api/hot?limit=12')
.then(r => r.json());
// 埋点延后执行
setTimeout(() => {
initAnalytics();
}, 0);
2. 商品卡片:用 key + v-memo + 函数式组件,干掉无意义响应式
原来每张卡片都是独立的 Vue 组件,含 4 个 ref、2 个 computed、1 个 watch。30 张卡就是 30 套响应式系统。实测,光 setup() 执行就占了 1.2s。
换成函数式组件 + v-memo 后,卡片变成纯渲染函数,只依赖 props,没有响应式开销。再配合 :key="item.id + item.stock" 精准控制更新粒度,哪怕 stock 变了,也只重绘那一张卡。
<!-- ✅ 优化后:函数式组件 + v-memo -->
<template v-for="item in items" :key="item.id + item.stock">
<ProductCard
v-memo="[item.id, item.price, item.stock]"
:item="item"
/>
</template>
ProductCard 是个 defineComponent({ setup() { return () => ... } }),没 data、没 watch、没生命周期——就是个 JS 函数。
3. 滚动监听:换用 IntersectionObserver,彻底告别 scroll 事件
上面那段 scroll 监听器,删掉,一行都不留。换成原生 IntersectionObserver,内存占用降了 60%,而且自动节流,兼容性也 OK(iOS 13.4+ / Android Chrome 51+)。
// ✅ 优化后:用 IntersectionObserver 替代 scroll 监听
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const item = entry.target;
item.dataset.loaded = 'true';
// 触发图片加载或数据请求
loadImage(item.dataset.src);
}
});
},
{ threshold: 0.1 }
);
items.forEach(el => observer.observe(el));
注意:这里没用 Vue 的 v-intersection 这类封装库——自己写 10 行就够了,加库反而多 3KB gzip,不值。
优化后:流畅多了
改完上线,红米 Note 10 上跑下来:
- 首屏加载时间:5.2s → 820ms(↓84%)
- 首屏渲染时间:4.8s → 790ms
- LCP:6.1s → 850ms
- CLS:0.32 → 0.003(几乎为 0)
- 内存峰值:128MB → 43MB
运营说“终于能点了”,测试同学说“滑动没掉帧了”,连 QA 都主动给我点了杯咖啡(虽然可能是想让我别再改了)。
当然也不是完美。比如 iOS Safari 下 IntersectionObserver 对 position: fixed 元素偶尔失焦,我们加了 fallback:检测到不支持时,降级为 passive 的 scroll 事件 + requestIdleCallback 包裹判断逻辑。这部分代码没放进来,太细了,真要用可以留言我贴出来。
性能数据对比
这是压测环境(模拟 3G 网络 + CPU 4x slowdown)下的真实 Lighthouse 报告对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| FCP | 4.1s | 0.68s | ↑83% |
| TTFB | 1.4s | 1.35s | ↑4% |
| LCP | 6.1s | 0.85s | ↑86% |
| 交互可操作时间(TTI) | 7.2s | 1.1s | ↑85% |
说实话,TTFB 提升不大,说明瓶颈真不在网络,而在 JS 执行和渲染。这点确认得很清楚。
最后说两句
这次优化没碰 Webpack 配置,没上 Service Worker,没搞 SSR,就是盯着 DevTools 里那几条红色长条,一条一条往下砍。很多所谓“最佳实践”,放到具体项目里就是过度设计。比起“用对技术”,我更相信“用少一点技术”。
踩坑提醒:别信“所有组件都要用 Composition API”,别觉得“所有请求都该封装成 useXXX”,也别一上来就配 build.rollupOptions.treeshake——先把 console.log 和 v-for 干干净净地扫一遍再说。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,尤其是 IntersectionObserver 在 iOS 的兼容处理,我还在找更稳的解法。

暂无评论