项目配置优化的实战技巧与性能提升秘诀
优化前:卡得不行
上周上线了一个新功能,页面里要加载几百个动态卡片,每个都带图片、标签和交互。本以为用 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。

暂无评论