长列表渲染方案
长列表渲染的挑战
长列表渲染是前端开发中常见的性能瓶颈之一。当页面需要展示大量数据时,传统的渲染方式会导致严重的性能问题,包括页面卡顿、内存占用过高甚至浏览器崩溃。这些问题主要源于DOM节点的过度创建和渲染。
虚拟滚动原理
虚拟滚动通过只渲染可视区域内的元素来解决长列表性能问题。其核心思想是:
- 计算可视区域高度
- 根据滚动位置确定当前可见的元素范围
- 只渲染这些可见元素
- 使用占位元素保持列表总高度
// 基本虚拟滚动实现原理
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