阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 长列表渲染方案

长列表渲染方案

作者:陈川 阅读数:32530人阅读 分类: Vue.js

长列表渲染的挑战

长列表渲染是前端开发中常见的性能瓶颈之一。当页面需要展示大量数据时,传统的渲染方式会导致严重的性能问题,包括页面卡顿、内存占用过高甚至浏览器崩溃。这些问题主要源于DOM节点的过度创建和渲染。

虚拟滚动原理

虚拟滚动通过只渲染可视区域内的元素来解决长列表性能问题。其核心思想是:

  1. 计算可视区域高度
  2. 根据滚动位置确定当前可见的元素范围
  3. 只渲染这些可见元素
  4. 使用占位元素保持列表总高度
// 基本虚拟滚动实现原理
function renderVisibleItems() {
  const scrollTop = container.scrollTop
  const visibleStartIndex = Math.floor(scrollTop / itemHeight)
  const visibleEndIndex = Math.min(
    visibleStartIndex + Math.ceil(containerHeight / itemHeight),
    totalItems - 1
  )
  
  // 只渲染可见项
  items.slice(visibleStartIndex, visibleEndIndex).forEach(item => {
    renderItem(item)
  })
  
  // 设置占位元素高度
  placeholder.style.height = `${(totalItems - visibleEndIndex) * itemHeight}px`
}

Vue实现方案

使用vue-virtual-scroller

vue-virtual-scroller是Vue生态中成熟的虚拟滚动解决方案:

import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

export default {
  components: { RecycleScroller },
  data() {
    return {
      items: Array(10000).fill().map((_, i) => ({ id: i, text: `Item ${i}` }))
    }
  }
}
<template>
  <RecycleScroller
    class="scroller"
    :items="items"
    :item-size="50"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="item">
      {{ item.text }}
    </div>
  </RecycleScroller>
</template>

<style>
.scroller {
  height: 500px;
}
.item {
  height: 50px;
  padding: 10px;
}
</style>

自定义虚拟滚动组件

对于更定制化的需求,可以手动实现虚拟滚动:

export default {
  data() {
    return {
      items: [], // 大数据源
      visibleItems: [], // 当前可见项
      startIndex: 0,
      endIndex: 0,
      itemHeight: 50,
      scrollTop: 0
    }
  },
  mounted() {
    this.calculateVisibleItems()
    window.addEventListener('scroll', this.handleScroll)
  },
  methods: {
    handleScroll() {
      this.scrollTop = window.scrollY
      this.calculateVisibleItems()
    },
    calculateVisibleItems() {
      const viewportHeight = window.innerHeight
      this.startIndex = Math.floor(this.scrollTop / this.itemHeight)
      this.endIndex = Math.min(
        this.startIndex + Math.ceil(viewportHeight / this.itemHeight),
        this.items.length - 1
      )
      this.visibleItems = this.items.slice(this.startIndex, this.endIndex + 1)
    }
  }
}
<template>
  <div class="virtual-list" :style="{ height: totalHeight + 'px' }">
    <div 
      class="list-container" 
      :style="{ transform: `translateY(${startIndex * itemHeight}px)` }"
    >
      <div 
        v-for="item in visibleItems" 
        :key="item.id" 
        class="list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

分页加载与无限滚动

基础分页实现

export default {
  data() {
    return {
      items: [],
      currentPage: 1,
      pageSize: 50,
      isLoading: false,
      hasMore: true
    }
  },
  methods: {
    async loadMore() {
      if (this.isLoading || !this.hasMore) return
      
      this.isLoading = true
      try {
        const newItems = await fetchItems(this.currentPage, this.pageSize)
        this.items = [...this.items, ...newItems]
        this.currentPage++
        this.hasMore = newItems.length === this.pageSize
      } finally {
        this.isLoading = false
      }
    },
    handleScroll() {
      const { scrollTop, scrollHeight, clientHeight } = document.documentElement
      if (scrollHeight - (scrollTop + clientHeight) < 100) {
        this.loadMore()
      }
    }
  },
  mounted() {
    window.addEventListener('scroll', this.handleScroll)
    this.loadMore()
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.handleScroll)
  }
}

结合虚拟滚动的无限加载

export default {
  data() {
    return {
      items: [],
      visibleItems: [],
      startIndex: 0,
      endIndex: 20, // 初始可见项数量
      itemHeight: 50,
      isLoading: false,
      hasMore: true
    }
  },
  methods: {
    async loadMore() {
      if (this.isLoading || !this.hasMore) return
      
      this.isLoading = true
      try {
        const newItems = await fetchItems()
        this.items = [...this.items, ...newItems]
        this.hasMore = newItems.length > 0
      } finally {
        this.isLoading = false
      }
    },
    handleScroll() {
      const { scrollTop } = document.documentElement
      const viewportHeight = window.innerHeight
      
      // 计算新的可见范围
      this.startIndex = Math.floor(scrollTop / this.itemHeight)
      this.endIndex = Math.min(
        this.startIndex + Math.ceil(viewportHeight / this.itemHeight) + 5, // 预加载缓冲
        this.items.length - 1
      )
      
      this.visibleItems = this.items.slice(this.startIndex, this.endIndex + 1)
      
      // 接近底部时加载更多
      if (this.endIndex > this.items.length - 10 && this.hasMore && !this.isLoading) {
        this.loadMore()
      }
    }
  }
}

性能优化技巧

使用Object.freeze

export default {
  async created() {
    const data = await fetchLargeData()
    this.items = Object.freeze(data) // 冻结数据避免Vue响应式开销
  }
}

避免不必要的重新渲染

export default {
  components: {
    ItemComponent: {
      props: ['item'],
      template: `<div>{{ item.content }}</div>`,
      // 只有item.id变化时才重新渲染
      computed: {
        nonReactiveProps() {
          return { id: this.item.id }
        }
      }
    }
  }
}

使用Web Worker处理大数据

// worker.js
self.onmessage = function(e) {
  const { data, startIndex, endIndex } = e.data
  const visibleItems = data.slice(startIndex, endIndex + 1)
  self.postMessage(visibleItems)
}

// Vue组件
export default {
  data() {
    return {
      worker: new Worker('worker.js'),
      items: [],
      visibleItems: []
    }
  },
  created() {
    this.worker.onmessage = (e) => {
      this.visibleItems = e.data
    }
  },
  methods: {
    updateVisibleItems(startIndex, endIndex) {
      this.worker.postMessage({
        data: this.items,
        startIndex,
        endIndex
      })
    }
  },
  beforeDestroy() {
    this.worker.terminate()
  }
}

特殊场景处理

动态高度项目

export default {
  data() {
    return {
      items: [],
      itemHeights: [], // 存储每个项目实际高度
      estimatedItemHeight: 50 // 预估高度
    }
  },
  methods: {
    calculateVisibleItems() {
      // 使用二分查找确定startIndex
      let low = 0
      let high = this.items.length - 1
      let startIndex = 0
      const scrollTop = this.scrollTop
      
      while (low <= high) {
        const mid = Math.floor((low + high) / 2)
        const itemOffset = this.getItemOffset(mid)
        
        if (itemOffset < scrollTop) {
          startIndex = mid
          low = mid + 1
        } else {
          high = mid - 1
        }
      }
      
      // 计算endIndex...
    },
    getItemOffset(index) {
      // 如果有记录实际高度则使用实际高度,否则使用预估高度
      return this.itemHeights
        .slice(0, index)
        .reduce((sum, height) => sum + (height || this.estimatedItemHeight), 0)
    },
    updateItemHeight(index, height) {
      this.$set(this.itemHeights, index, height)
    }
  }
}
<template>
  <div v-for="(item, index) in visibleItems" :key="item.id" :ref="`item-${index}`">
    <!-- 内容 -->
  </div>
</template>

<script>
export default {
  updated() {
    this.$nextTick(() => {
      this.visibleItems.forEach((_, index) => {
        const el = this.$refs[`item-${index}`][0]
        if (el) {
          const height = el.getBoundingClientRect().height
          this.updateItemHeight(this.startIndex + index, height)
        }
      })
    })
  }
}
</script>

分组渲染

对于特别复杂的列表项,可以考虑分组渲染:

export default {
  data() {
    return {
      items: [],
      visibleGroups: [],
      groupSize: 10, // 每组10个项目
      startGroupIndex: 0,
      endGroupIndex: 0
    }
  },
  computed: {
    groups() {
      const groups = []
      for (let i = 0; i < this.items.length; i += this.groupSize) {
        groups.push(this.items.slice(i, i + this.groupSize))
      }
      return groups
    }
  },
  methods: {
    updateVisibleGroups() {
      const scrollTop = this.scrollTop
      const viewportHeight = this.viewportHeight
      const groupHeight = this.groupSize * this.estimatedItemHeight
      
      this.startGroupIndex = Math.floor(scrollTop / groupHeight)
      this.endGroupIndex = Math.min(
        this.startGroupIndex + Math.ceil(viewportHeight / groupHeight) + 1,
        this.groups.length - 1
      )
      
      this.visibleGroups = this.groups.slice(
        this.startGroupIndex,
        this.endGroupIndex + 1
      )
    }
  }
}

测试与监控

实现虚拟滚动后,需要建立性能监控机制:

// 性能监控组件
export default {
  data() {
    return {
      fps: 0,
      lastTime: 0,
      frameCount: 0,
      memoryUsage: 0
    }
  },
  mounted() {
    this.monitorPerformance()
    setInterval(() => {
      if (performance.memory) {
        this.memoryUsage = performance.memory.usedJSHeapSize / 1024 / 1024
      }
    }, 1000)
  },
  methods: {
    monitorPerformance() {
      requestAnimationFrame(() => {
        const now = performance.now()
        if (this.lastTime) {
          this.frameCount++
          if (now > this.lastTime + 1000) {
            this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime))
            this.frameCount = 0
            this.lastTime = now
          }
        } else {
          this.lastTime = now
        }
        this.monitorPerformance()
      })
    }
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:内存管理建议

下一篇:动画性能优化

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌