前端性能优化实战总结,这些坑我都踩过了
一个电商项目的真实性能优化记录
最近做了一个电商项目的前端重构,说实话,开始没想到会遇到这么多性能问题。项目原本是个老系统,用户量上来之后各种卡顿,尤其是商品列表页和搜索页面,用户体验差得不行。老板催着要优化,我就接了这个活。
项目用的是Vue 2 + Element UI,数据量大概每页展示50-100个商品卡片,加上筛选、排序这些功能,DOM节点数量很容易就上万了。刚开始接手的时候,随便滑动几下滚动条,Chrome的任务管理器内存占用蹭蹭往上涨,FPS直接掉到个位数。
Virtual Scroll的尝试和翻车
第一个想到的就是virtual scroll,毕竟这是解决大量列表渲染的经典方案。我找了几个库试了试,v-virtual-scroll-list看起来还不错。
// 最初的virtual scroll实现
import VirtualList from 'vue-virtual-scroll-list'
export default {
components: {
VirtualList
},
data() {
return {
allItems: [], // 一万多条数据
itemHeight: 120,
containerHeight: 600
}
},
methods: {
getItem(item, index) {
return (
<div class="product-card">
{/* 商品信息 */}
{this.renderProductCard(item)}
</div>
)
}
},
render(h) {
return (
<VirtualList
style="height: 600px; overflow-y: auto;"
:items="allItems"
:item-height="itemHeight"
:container-height="containerHeight"
:on-item-click="handleItemClick"
>
{this.getItem}
</VirtualList>
)
}
}
代码写完跑起来,发现问题了。商品卡片里面有很多动态内容,价格变动、库存更新、促销活动这些,虚拟滚动虽然解决了DOM节点数量的问题,但是数据更新的响应变得很慢。每次后端推新数据过来,要卡好几秒才能更新到视图上,而且还有显示错乱的情况。
折腾了半天,发现是virtual scroll的diff算法和我们的业务场景不太匹配。商品卡片的复杂度比普通的文本列表高太多了,每次重新渲染都涉及到大量的计算和重排。
分片渲染的大胆尝试
后来想到了分片渲染,就是把大数据集分成多个小批次,在空闲时间分批渲染。这部分确实踩了不少坑。
// 分片渲染的核心实现
export default {
data() {
return {
allProducts: [],
displayedProducts: [],
batchSize: 20,
currentIndex: 0,
isRendering: false
}
},
methods: {
async renderBatch() {
if (this.isRendering || this.currentIndex >= this.allProducts.length) {
this.isRendering = false
return
}
this.isRendering = true
// 分批添加数据
const batch = this.allProducts.slice(
this.currentIndex,
this.currentIndex + this.batchSize
)
this.displayedProducts.push(...batch)
this.currentIndex += this.batchSize
// 让出主线程
await this.nextFrame()
this.$nextTick(() => {
this.renderBatch()
})
},
nextFrame() {
return new Promise(resolve => {
requestAnimationFrame(() => resolve())
})
},
// 防抖的滚动处理
handleScroll: _.debounce(function(e) {
const element = e.target
const bottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 100
if (bottom && !this.loadingMore && this.hasMore) {
this.loadMore()
}
}, 100)
}
}
这个方案比virtual scroll稳定多了,但也有问题。主要是用户体验不太好,数据是一段一段出来的,有种”加载中”的感觉。而且当用户快速滚动时,可能会跳过某些还未渲染的区域,导致空白。
缓存策略的深度优化
最大的坑其实是在缓存这里。项目里有很多重复的商品信息需要请求,比如店铺评分、商品标签这些。最开始每次渲染都去请求一次,结果API被我们刷爆了。
// 图片懒加载和缓存优化
export class ImageCacheManager {
constructor() {
this.cache = new Map()
this.observer = null
this.initIntersectionObserver()
}
initIntersectionObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
const src = img.dataset.src
if (src && !this.cache.has(src)) {
this.preloadImage(src).then(blob => {
this.cache.set(src, URL.createObjectURL(blob))
img.src = this.cache.get(src)
img.classList.remove('lazy-loading')
})
} else if (this.cache.has(src)) {
img.src = this.cache.get(src)
}
this.observer.unobserve(img)
}
})
}, {
rootMargin: '50px'
})
}
async preloadImage(src) {
// 缓存商品图片,避免重复请求
try {
const response = await fetch(src)
return response.blob()
} catch (error) {
console.error('Image preload failed:', error)
}
}
observe(imageElement) {
this.observer.observe(imageElement)
}
}
// 在组件中的使用
mounted() {
this.imageCache = new ImageCacheManager()
this.$nextTick(() => {
document.querySelectorAll('.lazy-image').forEach(img => {
this.imageCache.observe(img)
})
})
}
这里踩坑最多的地方是内存泄漏。最初缓存没设限制,长时间使用下来内存占用越来越高。后来加了LRU算法,超过一定数量就清理最久未使用的缓存项。
// LRU缓存实现
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize
this.cache = new Map()
}
get(key) {
if (!this.cache.has(key)) return null
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}
实际效果和遗留问题
经过这一轮优化,页面性能提升明显。滚动流畅度提高了80%,内存占用稳定在合理范围内,API请求次数减少了60%。用户反馈也好了不少,至少不会再出现卡死的情况。
不过还是有些小问题。快速滚动时偶尔会有图片闪烁,这个主要是因为图片预加载策略还需要调整。还有就是复杂的筛选操作仍然有点卡,特别是在移动端设备上。
整体来说,这次优化还是达到了预期目标。性能优化这个事情真的没有标准答案,不同的业务场景需要不同的策略。现在回头看,如果一开始就能考虑到这些性能问题,架构设计可能会更合理一些。
以上是我踩坑后的总结,希望对你有帮助。

暂无评论