Seize the time to study, and then sleep 😪
preface
In our work, we sometimes encounter business requirements such as loading list data indefinitely and not using pagination to load list data, which we collectively refer to as long lists.
Why use virtual lists
Assuming that there are 10,000 pieces of data on the page, whether it is a one-time rendering or scrolling, there will always be a blank screen or a lag, especially in some low-configuration models. Below is wanzi said home waterfall flow list of rolling recording screen.
You can obviously see a white screen, or even a lag, during fast scrolling. This is because when the list data is large, the page renders too many nodes, and these nodes contain child nodes, which can consume a lot of performance, and in small programs can cause flashbacks due to insufficient memory.
Virtual lists are a solution to this problem.
What is a virtual list
A virtual list is simply an implementation of display on demand. The specific approach is: render only the visible area, render or not render the part of the non-visible area, so as to achieve extremely high rendering performance.
Assuming that the page needs to display 1000 pieces of data, the height of the viewable area of the page is 500px, and the height of the list items is 50px, the maximum viewable area of the page can only display 10 pieces of data, so we only need to render 10 pieces of data for the first time.
Next we’ll look at the list items that should be rendered in the visible area of the page by calculating the current scroll value when scrolling occurs.
If the scroll occurs and the scroll value is 100px, then the list items in the visible area can be calculated as items 3 through 12.
Deformation of waterfall flow list
Going back to my project, Wanzi said that the home page presents the list in the form of a waterfall, so the structure will be different from the list above. The following is the basic structure of the waterfall flow list.
Due to the special nature of the waterfall list structure, there is no way to use the above approach to implement a different approach.
A page requests 10 data, one data is considered as an article card, the article card according to the left and right columns which side of the height of the lower priority insert (so this scheme is only suitable for known data height), and consider a page of data is a screen, then the following structure can be obtained.
Since the height of each article card is not consistent, this creates a well-proportioned waterfall effect. But here each screen is separated, so the following happens:
To solve this situation, simply calculate the height difference between the left and right columns of the previous screen to obtain the offset of the next screen.
Realize the principle of
The implementation of waterfall virtual list is actually to render only the list items needed in the visual area when the first screen is loaded. When the page is scrolling, determine whether the content of the target node is in the visual area, and if so, render the content.
The IntersectionObserver object can be used to determine whether the target node has entered the visible area.
// index.wxml
<wxs module="filter">
var isInVisiblePages = function (visibleIndexs, current) {
return visibleIndexs.indexOf(current) > -1
}
var offsetTop = function (offset) {
return offset > 0
? 'top: -' + offset + 'px'
: ''
}
module.exports = {
isInVisiblePages: isInVisiblePages,
offsetTop: offsetTop
}
</wxs>
<view class="waterfull">
<view
class="waterfull__item"
wx:for="{{ records }}"
wx:key="index"
style="height: {{ item.height }}px"
data-index="{{ index }}">
<block wx:if="{{ filter.isInVisiblePages(visibleIndexs, index) }}">
<view
class="waterfull__item__left"
style="{{ filter.offsetTop(item.leftOffset) }}">
<template is="article" data="{{ data: item.leftData }}"></template>
</view>
<view
class="waterfull__item__right"
style="{{ filter.offsetTop(item.rightOffset) }}">
<template is="article" data="{{ data: item.rightData }}"></template>
</view>
</block>
</view>
</view>
<template name="article">
<view
class="article-box"
wx:for="{{ data }}"
wx:for-item="article"
wx:for-index="articleIndex"
wx:key="articleIndex">
<view class="article">
<view class="article__poster">
<image
class="article__poster__img"
src="{{ article.coverImage }}"
style="height: {{ article.realHeight }}rpx"
mode="aspectFill" />
</view>
<view class="article__content">
<view class="article__content__title">{{ article.title }}</view>
<view class="article__content__creator">
<image
class="article__content__creator__avatar"
src="{{ article.creator.avatar }}"
mode="aspectFill" />
<view class="article__content__creator__nickname">{{ article.creator.nickname }}</view>
</view>
<view
class="article__content__topic"
wx:if="{{ article.topic }}">
<image
class="article__content__topic__icon"
src="https://pub-img.perfectdiary.com/material/image/2021/05/5d604f0b034d4c7f9b857fb0919f3ee3.png" />
<view class="article__content__topic__title">{{ article.topic.title }}</view>
</view>
</view>
</view>
</view>
</template>
Copy the code
// index.wxss
.waterfull {
padding: 0 12rpx;
background: linear-gradient(180deg.#FFFFFF 0%.#F5F5F5 100%);
}
.waterfull__item {
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.waterfull__item__left..waterfull__item__right {
position: absolute;
top: 0;
width: calc(50% - 5rpx);
}
.waterfull__item__left {
left: 0;
}
.waterfull__item__right {
right: 0;
}
.article-box {
padding: 6rpx 0;
}
.article {
font-size: 0;
box-shadow: 0px 4rpx 16rpx 0px rgba(34.34.34.0.05); }...Copy the code
// index.js
const App = getApp()
let rtp = 0.5
let maxHeight = 238.7
let maxWidth = 179
const coverImgProportion = 0.75 // Cover image width to height ratio
const proportion = 0.477 // The ratio of the height of the waterfall cover to the screen
if (App.systemInfo && App.systemInfo.windowWidth) {
rtp = App.systemInfo.windowWidth / 750
maxWidth = App.systemInfo.windowWidth * proportion || maxWidth
maxHeight = (App.systemInfo.windowWidth * proportion) / coverImgProportion || maxHeight
maxHeight += 1
}
Component({
// The component's property list
properties: {
articles: {
type: Array.value: [],
observer (list) {
this.handleArticleData(list)
}
}
},
// The initial data of the component
data: {
records: []./ / total list
visibleIndexs: [].// A list of renderable indexes
},
lifetimes: {
detached () {
this.disconnect()
}
},
pageLifetimes: {
show () {
this.reconnect()
}
},
ready () {
this.createObserve()
},
// List of methods for the component
methods: {
// Process list data
handleArticleData (list) {
// Split component screen array 10
const _list = [...list]
const allList = []
while (_list.length) {
const currentList = _list.splice(0.10)
allList.push({
data: currentList
})
}
this.handleWaterfullList(allList)
},
handleWaterfullList (list) {
// All units are RPX
const titleHeight = 88 // Title height
const avatarHeight = 34 // The height of the avatar
const avatarMarginTop = 12 // head top margin
const topicHeight = 48 // Topic height
const topicMarginTop = 12 // Topic margin
const contentPaddingTop = 12 // Top margin of content
const contentPaddingBottom = 16 // The bottom margin of the content
const boxPaddingTop = 6 // Box top margin
const boxPaddingBottom = 6 // Box bottom spacing
// Fixed height set
const fixedHeight = [
titleHeight, avatarHeight, avatarMarginTop, contentPaddingTop,
contentPaddingBottom, boxPaddingTop, boxPaddingBottom
]
list.forEach((item, index) = > {
const isLast = index + 1 === list.length
// The height is subtracted from the offset
let leftHeight = 0 - item.leftOffset || 0
let rightHeight = 0 - item.rightOffset || 0
const leftData = []
const rightData = []
item.data.forEach(article= > {
article.realHeight = this.calcImageHeight(article)
const heights = [...fixedHeight, article.realHeight]
if (article.topic) {
heights.push(topicHeight, topicMarginTop)
}
// Calculate the card height
// For errors, convert each height to px and add
const cardHeight = heights.reduce((total, current) = > total + this.handleRtoP(current), 0)
article.cardHeight = cardHeight
// Calculate the height of the left and right columns
// Make sure the height difference between the left and right columns is not too big
if (leftHeight <= rightHeight) {
leftHeight += cardHeight
leftData.push(article)
} else {
rightHeight += cardHeight
rightData.push(article)
}
})
// Calculate the offset
if(! isLast) {const offset = Math.abs(leftHeight - rightHeight)
const nextIndex = index + 1
if (leftHeight >= rightHeight) {
list[nextIndex].rightOffset = offset
list[nextIndex].leftOffset = 0
} else {
list[nextIndex].leftOffset = offset
list[nextIndex].rightOffset = 0
}
}
item.height = Math.max(leftHeight, rightHeight)
item.leftData = leftData
item.rightData = rightData
})
this.setData({
records: list
}, () = > {
this.reconnect()
})
},
calcImageHeight (article) {
// Convert to the corresponding size according to the original size and proportion of the cover
// If the limit is exceeded, the maximum height is used
let imageHeight = maxHeight
if (article.imgHeight && article.imgWidth) {
imageHeight = (maxWidth * article.imgHeight) / article.imgWidth
}
if (imageHeight > maxHeight) {
imageHeight = maxHeight
}
// First convert to RPX
imageHeight = imageHeight / rtp
return imageHeight
},
handleRtoP (height) {
return parseInt(height * rtp)
},
// Create a visual area listener
createObserve () {
if (this.ob) return
this.ob = this.createIntersectionObserver({
observeAll: true.initialRatio: 0,
}).relativeToViewport({
bottom: 0
})
this.ob.observe('.waterfull__item'.res= > {
const { index } = res.dataset
if (res.intersectionRatio > 0) {
this.setData({
visibleIndexs: [index - 1, index, index + 1]
})
}
})
},
connect () {
this.createObserve()
},
// reconnect visual listener
reconnect () {
if (!this.ob) return
this.disconnect()
this.connect()
},
// Disconnect the visual listener
disconnect () {
if (!this.ob) return
this.ob.disconnect()
this.ob = null}}})Copy the code
The final effect is as follows:
Click to see the complete applet snippet
The online optimization results are as follows:
reference
- “Front-end Advanced” high performance rendering of 100,000 pieces of data (virtual list)