项目配置优化的实战技巧与性能提升秘诀

东方洋博 工具 阅读 1,289
赞 22 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上周上线了一个新功能,页面里要加载几百个动态卡片,每个都带图片、标签和交互。本以为用 Vue 做响应式更新挺稳的,结果一上测试环境,首屏加载直接 5 秒起步,滚动也卡得像 PPT。用户稍微滑两下,浏览器 tab 就提示“无响应”。我盯着 Performance 面板看了半天,内存一直在涨,FPS 掉到 20 以下,差点想关电脑跑路。

项目配置优化的实战技巧与性能提升秘诀

最离谱的是,数据其实不大,总共才 300 多条,接口返回不到 200KB,但页面就是跑不动。同事说可能是渲染太多节点了,但我一开始不信邪,觉得现代框架不至于这么弱。直到我自己拿手机试了下——直接白屏,真机卡死。这才意识到:问题不小,得动手了。

找到瘼颈了!

我先用 Chrome DevTools 的 Performance 打了个快照,刷新页面录了一段。分析后发现两个大问题:

  • 主线程被 大量连续的 DOM 操作 占满,尤其是初始渲染那一下,一口气插入 300+ 个 div
  • Vue 的响应式系统在初始化时对整个数据列表做了 getter/setter 劫持,这步就花了 1.2 秒

我还顺手查了内存,Heap Snapshot 显示有快 40MB 的对象驻留,很多是没被释放的 watcher 实例。看来不是简单的“内容多”,而是配置和实现方式有问题。

接着我上了 Vue Devtools,看组件树,发现父组件一更新,所有子 Card 都跟着 rerender,哪怕它们的数据根本没变。典型的没有做 shouldUpdate 控制。

动刀:懒加载 + 虚拟滚动

第一反应是上虚拟滚动。但我不想引入 big.js 那种重型库,毕竟只是垂直列表,自己搞个简易版更轻量。

核心思路很简单:只渲染视口内及附近(上下各缓冲 2 屏)的元素,其他用占位撑高度。这样 DOM 节点从 300+ 降到 20 个左右。

下面是关键代码:

// 简易虚拟滚动组件 VirtualList.vue
export default {
  props: {
    items: Array,
    itemHeight: { type: Number, default: 80 }
  },
  computed: {
    visibleStart() {
      return Math.max(0, this.scrollTop / this.itemHeight - 5)
    },
    visibleEnd() {
      return this.visibleStart + this.visibleCount + 10
    },
    visibleCount() {
      const viewportHeight = window.innerHeight
      return Math.ceil(viewportHeight / this.itemHeight)
    },
    renderedItems() {
      return this.items.slice(this.visibleStart, this.visibleEnd)
    },
    containerStyle() {
      return {
        height: ${this.items.length * this.itemHeight}px,
        position: 'relative'
      }
    },
    offsetStyle() {
      return {
        transform: translateY(${this.visibleStart * this.itemHeight}px)
      }
    }
  },
  data() {
    return {
      scrollTop: 0
    }
  },
  mounted() {
    this.$el.addEventListener('scroll', this.handleScroll, { passive: true })
  },
  beforeDestroy() {
    this.$el.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    handleScroll(e) {
      this.scrollTop = e.target.scrollTop
    }
  }
}
<!-- 使用方式 -->
<div ref="container" class="list-container">
  <div :style="containerStyle">
    <div :style="offsetStyle">
      <Card
        v-for="(item, index) in renderedItems"
        :key="item.id"
        :data="item"
        class="virtual-item"
      />
    </div>
  </div>
</div>

这里注意我踩过好几次坑:一开始用了 offsetTop 计算位置,结果每次滚动都要重排;后来改用 transform + slice 切片,性能直接起飞。还有事件监听加了 { passive: true },避免 touch 滚动被阻塞。

响应式优化:别让 Vue 劫持一切

另一个大头是数据初始化。原来我把整个 list 直接扔给 Vue 的 data:

data() {
  return {
    list: hugeData // 300+ 对象,每个都有 nested fields
  }
}

结果 Vue 递归遍历所有属性做 defineProperty 劫持,巨慢。后来改成标记为只读:

data() {
  return {
    list: Object.freeze(hugeData) // 冻结对象,跳过响应式处理
  }
}

再配合 track-by="id"shouldComponentUpdate 类似的机制(Vue 用 key),让 diff 更高效。这一改,初始化时间从 1.2s 降到 200ms 左右。

还有一个细节:我把 Card 组件加上了 shouldUpdate 控制:

// Card.vue
export default {
  props: ['data'],
  // 只有 data 改变才更新
  shouldComponentUpdate(nextProps) {
    return nextProps.data !== this.data
  }
}

图片也得管:懒加载 + 缩略图占位

卡片里的图片也是拖累。虽然用了 CDN,但一次性发 300 个 HTTP 请求,DNS 都扛不住。

解决方案:

  • 图片使用 IntersectionObserver 做懒加载
  • 默认显示 base64 缩略图,加载完成后再替换
// ImageLazy.vue
export default {
  props: ['src'],
  data() {
    return {
      loaded: false
    }
  },
  mounted() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = new Image()
          img.src = this.src
          img.onload = () => {
            this.loaded = true
            observer.disconnect()
          }
        }
      })
    })
    observer.observe(this.$el)
  }
}

优化后:流畅多了

改完当天我就部署到预发环境测了下。首屏加载时间从平均 5.1s 降到 800ms,FPS 稳定在 50~60,内存占用从峰值 120MB 降到 40MB 左右。最关键的是,手机端终于不卡死了,滑动跟手。

我还跑了 Lighthouse,Performance 分数从 38 跑到了 89。Accessibility 和 Best Practices 也因为加了 alt、role 等提了一波分。

性能数据对比

这是优化前后的关键指标对比:

  • 首屏渲染时间:5.1s → 800ms
  • 主线程阻塞时间:累计 2.3s → 0.4s
  • 内存占用峰值:120MB → 40MB
  • Lighthouse 性能分:38 → 89
  • 可交互时间(TTI):6.2s → 1.1s

当然还有些小问题,比如快速滚动时偶尔会闪一下空白,是因为缓冲区不够。可以调大 visibleCount,但会多渲染几个节点,权衡之下我觉得当前值够用。

总结:别迷信框架,默认配置往往不是最优的

这次优化折腾了半天发现,很多性能问题都不是“技术不够”,而是“配置太糙”。Vue 很强大,但你把 300 个非响应式数据扔进去,默认就会全劫持一遍。你不干预,它就不知道你要啥。

核心经验就三点:

  • 大数据列表必须上虚拟滚动,哪怕是简化版
  • 静态数据记得 Object.freeze,省掉一半初始化开销
  • 资源加载控制节奏,别一股脑全请求

这些改动加起来代码不超过 100 行,但效果立竿见影。有时候性能优化不需要换架构、上微前端,就把现有配置理一遍,就能救回来。

以上是我的优化经验,有更优的实现方式欢迎评论区交流

这个方案不是最优的,但最简单,适合中短期项目快速落地。如果是长期维护的项目,可能会考虑用 react-window 那类更成熟的方案。不过目前这套已经能满足需求了。

顺便提醒:IntersectionObserver 在低端安卓机上有兼容问题,我们项目支持到 Android 8+,所以没问题。如果你要保更低版本,建议加个 polyfill 或退化到 scroll event。

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

暂无评论