阿里云主机折上折
  • 微信号
Current Site:Index > Long list rendering solution

Long list rendering solution

Author:Chuan Chen 阅读数:32529人阅读 分类: Vue.js

Challenges of Long List Rendering

Long list rendering is a common performance bottleneck in front-end development. When a page needs to display a large amount of data, traditional rendering methods can cause severe performance issues, including page lag, high memory usage, and even browser crashes. These problems primarily stem from the excessive creation and rendering of DOM nodes.

Virtual Scrolling Principle

Virtual scrolling addresses the performance issues of long lists by rendering only the elements within the visible area. Its core concepts are:

  1. Calculate the height of the visible area
  2. Determine the range of currently visible elements based on the scroll position
  3. Render only these visible elements
  4. Use placeholder elements to maintain the total height of the list
// Basic virtual scrolling implementation principle
function renderVisibleItems() {
  const scrollTop = container.scrollTop
  const visibleStartIndex = Math.floor(scrollTop / itemHeight)
  const visibleEndIndex = Math.min(
    visibleStartIndex + Math.ceil(containerHeight / itemHeight),
    totalItems - 1
  )
  
  // Render only visible items
  items.slice(visibleStartIndex, visibleEndIndex).forEach(item => {
    renderItem(item)
  })
  
  // Set placeholder height
  placeholder.style.height = `${(totalItems - visibleEndIndex) * itemHeight}px`
}

Vue Implementation Solutions

Using vue-virtual-scroller

vue-virtual-scroller is a mature virtual scrolling solution in the Vue ecosystem:

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>

Custom Virtual Scrolling Component

For more customized needs, you can manually implement virtual scrolling:

export default {
  data() {
    return {
      items: [], // Large data source
      visibleItems: [], // Currently visible items
      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>

Pagination Loading and Infinite Scrolling

Basic Pagination Implementation

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)
  }
}

Infinite Loading with Virtual Scrolling

export default {
  data() {
    return {
      items: [],
      visibleItems: [],
      startIndex: 0,
      endIndex: 20, // Initial number of visible items
      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
      
      // Calculate new visible range
      this.startIndex = Math.floor(scrollTop / this.itemHeight)
      this.endIndex = Math.min(
        this.startIndex + Math.ceil(viewportHeight / this.itemHeight) + 5, // Pre-load buffer
        this.items.length - 1
      )
      
      this.visibleItems = this.items.slice(this.startIndex, this.endIndex + 1)
      
      // Load more when nearing the bottom
      if (this.endIndex > this.items.length - 10 && this.hasMore && !this.isLoading) {
        this.loadMore()
      }
    }
  }
}

Performance Optimization Techniques

Using Object.freeze

export default {
  async created() {
    const data = await fetchLargeData()
    this.items = Object.freeze(data) // Freeze data to avoid Vue reactivity overhead
  }
}

Avoiding Unnecessary Re-renders

export default {
  components: {
    ItemComponent: {
      props: ['item'],
      template: `<div>{{ item.content }}</div>`,
      // Re-render only when item.id changes
      computed: {
        nonReactiveProps() {
          return { id: this.item.id }
        }
      }
    }
  }
}

Using Web Workers for Large Data Processing

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

// Vue component
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()
  }
}

Special Scenario Handling

Dynamic Height Items

export default {
  data() {
    return {
      items: [],
      itemHeights: [], // Store actual height of each item
      estimatedItemHeight: 50 // Estimated height
    }
  },
  methods: {
    calculateVisibleItems() {
      // Use binary search to determine 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
        }
      }
      
      // Calculate endIndex...
    },
    getItemOffset(index) {
      // Use actual height if recorded, otherwise use estimated height
      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}`">
    <!-- Content -->
  </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>

Group Rendering

For particularly complex list items, consider group rendering:

export default {
  data() {
    return {
      items: [],
      visibleGroups: [],
      groupSize: 10, // 10 items per group
      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
      )
    }
  }
}

Testing and Monitoring

After implementing virtual scrolling, establish a performance monitoring mechanism:

// Performance monitoring component
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

上一篇:内存管理建议

下一篇:动画性能优化

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.