前端开发中那些真正管用的Best Practices实战总结

欣怡🍀 工具 阅读 3,011
赞 32 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上周上线了个新活动页,用 Vue 3 + Vite 搭的,页面结构也不复杂:顶部 Banner、三组商品卡片、底部一个表单。结果一上线,运营同事直接微信轰炸我——“点不动”“滑两下就白屏”“安卓机点开要等 5 秒才出内容”。我自己测了下,红米 Note 10(中低端 Android)上首次加载时间 5.2s,首屏渲染 4.8s,LCP 6.1s,CLS 高到报警……不是“有点慢”,是“根本没法用”。

前端开发中那些真正管用的Best Practices实战总结

更离谱的是,用户还没开始操作,控制台 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.logv-for 干干净净地扫一遍再说。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,尤其是 IntersectionObserver 在 iOS 的兼容处理,我还在找更稳的解法。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论