前端性能优化实战总结,这些坑我都踩过了

打工人晓莉 框架 阅读 1,481
赞 12 收藏
二维码
手机扫码查看
反馈

一个电商项目的真实性能优化记录

最近做了一个电商项目的前端重构,说实话,开始没想到会遇到这么多性能问题。项目原本是个老系统,用户量上来之后各种卡顿,尤其是商品列表页和搜索页面,用户体验差得不行。老板催着要优化,我就接了这个活。

前端性能优化实战总结,这些坑我都踩过了

项目用的是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%。用户反馈也好了不少,至少不会再出现卡死的情况。

不过还是有些小问题。快速滚动时偶尔会有图片闪烁,这个主要是因为图片预加载策略还需要调整。还有就是复杂的筛选操作仍然有点卡,特别是在移动端设备上。

整体来说,这次优化还是达到了预期目标。性能优化这个事情真的没有标准答案,不同的业务场景需要不同的策略。现在回头看,如果一开始就能考虑到这些性能问题,架构设计可能会更合理一些。

以上是我踩坑后的总结,希望对你有帮助。

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

暂无评论