前端性能优化实战我用这几个技巧让页面加载速度提升60%

慕容瑞雪 框架 阅读 1,960
赞 12 收藏
二维码
手机扫码查看
反馈

这次性能优化真的把我折腾惨了

最近接了个老项目重构的任务,说实话,接手的时候就知道会有不少坑。这个项目原本是个电商后台系统,用户量不算特别大,但响应速度慢得要命,特别是列表页面经常卡顿,用户体验很差。

前端性能优化实战我用这几个技巧让页面加载速度提升60%

项目初期做技术调研的时候,我就发现这个问题比想象中严重。Vue 2.x 版本,大量的 v-for 渲染,还有各种不必要的响应式数据监听。客户抱怨了好几个月,说页面打开要十几秒,表格滚动卡成PPT。我看了下代码,简直就是性能杀手集合体。

开始动手,发现比我想象的复杂

第一眼看到代码就想直接开干,把所有明显的性能问题修复一遍。结果开始动手才发现,这可不是简单的优化几个组件那么简单。

首先就是那个商品列表页面,用了 Vue 2.6.14,渲染几千条数据,而且每行都有复杂的交互功能。我测了一下,光是渲染就要5-8秒,滚动更是卡得不行。开始我以为是虚拟滚动的问题,后来发现连基础的数据绑定都存在问题。

原来的代码大概是这样:

// 原始代码,典型的性能杀手
export default {
  data() {
    return {
      productList: [], // 几千条数据
      selectedItems: []
    }
  },
  computed: {
    processedList() {
      return this.productList.map(item => {
        // 对每条数据进行复杂的计算
        return {
          ...item,
          formattedPrice: this.formatPrice(item.price),
          statusText: this.getStatusText(item.status),
          // 还有一堆复杂的处理
        }
      })
    }
  },
  methods: {
    formatPrice(price) {
      // 复杂的格式化逻辑
      return new Intl.NumberFormat().format(price)
    }
  }
}

第一个坑:大量不必要的响应式数据

最开始我犯了个错误,想当然地认为把所有数据都放在 data 里就好了。结果发现每次数据更新都会触发整个列表的重新渲染,特别是那个 computed 属性,每次稍微改动一个数据,整个列表就重新计算一遍。

后来调整了方案,把不需要响应式的静态数据移到了组件外部:

// 将不需要响应的数据移出data
const staticConfig = {
  currency: 'CNY',
  decimals: 2
}

export default {
  data() {
    return {
      productList: [],
      selectedIds: new Set()
    }
  },
  created() {
    // 非响应式数据存储
    this.staticConfig = staticConfig
  },
  computed: {
    processedList() {
      return this.productList.map(item => {
        return {
          ...item,
          // 简化的计算逻辑
          formattedPrice: this.getCachedFormattedPrice(item.id, item.price)
        }
      })
    }
  },
  methods: {
    getCachedFormattedPrice(id, price) {
      if (!this.priceCache) {
        this.priceCache = new Map()
      }
      
      const cacheKey = ${id}-${price}
      if (this.priceCache.has(cacheKey)) {
        return this.priceCache.get(cacheKey)
      }
      
      const formatted = new Intl.NumberFormat().format(price)
      this.priceCache.set(cacheKey, formatted)
      return formatted
    }
  }
}

第二个坑:虚拟滚动的兼容性问题

处理完响应式数据的问题,接下来就是虚拟滚动了。原来的想法很简单,引入一个虚拟滚动组件,搞定渲染性能问题。但是折腾半天发现,业务逻辑太复杂,很多交互功能在虚拟滚动下都不工作了。

原来的表格有很多嵌套的组件,每行都有编辑按钮、下拉选择框等等。用虚拟滚动之后,这些动态组件的状态管理变得很麻烦。开始没想到这点,后来调整了好几版才勉强跑起来。

<!-- 虚拟滚动组件实现 -->
<template>
  <div class="virtual-list" @scroll="handleScroll">
    <div :style="{ height: totalHeight + 'px' }" class="scroll-area">
      <div 
        :style="{ transform: translateY(${offsetTop}px) }" 
        class="visible-items"
      >
        <div 
          v-for="item in visibleItems" 
          :key="item.id"
          class="list-item"
        >
          <product-row 
            :product="item"
            @edit="handleEdit"
            @select="toggleSelection"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualProductList',
  props: {
    items: Array,
    itemHeight: {
      type: Number,
      default: 80
    }
  },
  data() {
    return {
      scrollTop: 0,
      containerHeight: 400
    }
  },
  computed: {
    totalHeight() {
      return this.items.length * this.itemHeight
    },
    startIndex() {
      return Math.floor(this.scrollTop / this.itemHeight)
    },
    endIndex() {
      const maxVisible = Math.ceil(this.containerHeight / this.itemHeight)
      return Math.min(this.startIndex + maxVisible + 5, this.items.length)
    },
    visibleItems() {
      return this.items.slice(this.startIndex, this.endIndex)
    },
    offsetTop() {
      return this.startIndex * this.itemHeight
    }
  },
  methods: {
    handleScroll(e) {
      this.scrollTop = e.target.scrollTop
    }
  }
}
</script>

第三个坑:缓存策略的权衡

缓存这部分确实让我纠结了很久。既要保证数据的实时性,又要避免重复计算,还要考虑内存占用。开始搞了一个复杂的缓存机制,结果发现缓存命中率不高,反而增加了复杂度。

后来简化了方案,只对一些计算成本高的数据做缓存:

methods: {
  initPerformanceOptimizations() {
    // 数据分页加载
    this.loadDataInChunks()
    
    // 计算属性缓存
    this.setupComputedCache()
    
    // 防抖处理
    this.debouncedUpdate = this.debounce(this.updateView, 16)
  },
  
  loadDataInChunks() {
    const chunkSize = 100
    let index = 0
    
    const loadChunk = () => {
      const chunk = this.rawData.slice(index, index + chunkSize)
      this.processedData.push(...chunk)
      index += chunkSize
      
      if (index < this.rawData.length) {
        requestAnimationFrame(loadChunk)
      }
    }
    
    loadChunk()
  },
  
  debounce(func, wait) {
    let timeout
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout)
        func(...args)
      }
      clearTimeout(timeout)
      timeout = setTimeout(later, wait)
    }
  }
}

最终效果还行,但不是完美

经过这些调整,页面性能确实提升了不少。渲染时间从原来的5-8秒降低到了1-2秒,滚动也基本流畅了。不过说实话,还有一些细节问题没完全解决。

比如在低端设备上偶尔还是会有卡顿,特别是在快速滚动的时候。还有内存占用虽然优化了一些,但对于超大数据集来说,还是会占用不少内存。但是客户那边验收通过了,这些问题暂时也就先这样了。

整体来说,这次优化让我对Vue性能优化有了更深的认识。以前总觉得虚拟滚动就能解决一切问题,实际上还是要根据具体场景来调整。每个优化点都可能带来新的问题,需要反复测试和调整。

一点小结

性能优化真的是个细致活儿,每个项目的情况都不一样。这次主要是解决了响应式数据滥用和渲染性能的问题,但还有更多可以优化的地方,比如网络请求的并发控制、组件懒加载等等。

以上是我这次性能优化的一些经验分享,希望对你有帮助。有什么更好的方案也欢迎交流讨论。

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

暂无评论