Long list rendering solution
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:
- Calculate the height of the visible area
- Determine the range of currently visible elements based on the scroll position
- Render only these visible elements
- 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