swiper渲染大量数据的优化方案
最近都在开发小程序相关业务,许多交互都需要类
Swiper
这种效果。秉持着不重复造轮子的原则,能用Swiper
实现的都用Swiper
实现了嘿嘿。但是过程中也暴露出一些问题,在某些数据量很大的场景下会渲染出多个swiper-item
,比如仿抖音那种刷视频效果,以及答题的业务下。都会涉及到数据量庞大,Swiper-item
渲染过多导致页面卡顿的问题。优化行动刻不容缓!下面就一起来看看该如何优化吧。
# 简易版优化
提示
先上个开胃菜,来个简易版的。大体的思路就是忽略swiper-item
的数量,只渲染当前项的内容,非当前项的swiper-item
内容为空。
上代码
export default function SwiperCard(){
const [currentIndex,setCurrentIndex] = useState(0)
const [list,setList] = useState([])
return (
<Swiper
className='video-swiper'
vertical
current={currentIndex}
>
{
list.map((item,index)=>{
return (
<swiper-item>
{
currentIndex===index&&<div>内容...</div>
}
</swiper-item>
)
})
}
</Swiper>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这个比较简单 就不过多解释了。下面来看看进阶版
# 进阶版优化
提示
简易版已经可以满足大部分场景,如果还要追求进一步优化的话就要从swiper-item
的数量入手了。下面我们将只渲染3
个swiper-item
,通过替换数据的方式来实现无限轮播
export default function SwiperCard(){
const [currentIndex,setCurrentIndex] = useState(0)
const [list,setList] = useState([])
return(
<Swiper
className='video-swiper'
vertical
current={currentIndex}
>
{
list.map((item,index)=>{
return (
<swiper-item>
{
currentIndex===index&&<div>内容...</div>
}
</swiper-item>
)
})
}
</Swiper>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
首先我们渲染的数据就不再是整个list
了,而是当前渲染项加上前一个和后一个数据组成的长度为3
的数组。所以我们要动态的计算出这个值
const [currentIndex,setCurrentIndex] = useState(0)
const [displayList,setDisplayList] = useState([]) // 真实渲染的数据
const [originList, setOriginList] = useState([]) // 原始数据
const [displayIndex,setDisplayIndex] = useState(0)// 当前展示项在原始数据中的索引
// 动态计算真实渲染的数据
useEffect(() => {
// 处理边界情况
if (displayIndex === 0) {
const nextList = originList.slice(0, 3)
setDisplayVideos([
...nextList
])
return
}
if (displayIndex === originList.length - 1) {
const nextList = originList.slice(-3)
setDisplayVideos([
...nextList
])
return
}
setDisplayList([
originList[displayIndex - 1],
originList[displayIndex],
originList[displayIndex + 1],
])
}, [displayIndex]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里我们引入了两个个新变量来存储真实渲染的数据
和当前展示项在原始数据中的索引
,并且通过监听displayIndex
的变化 动态计算DisplayList
的值。页面上不再渲染list
,而是渲染DisplayList
。
现在我们已经实现了只渲染3
个swiper-item
,还差点什么呢?仔细想想...对了!还没有更新currentIndex
的值呢。这一步就简单了我们只需要在swiper
的change
事件里去更新currentIndex
的值就可以了
const handleSwiperChange = (e)=>{
const { current } = e?.detail
if(current===0){
...
}else if(current===1){
...
}
}
2
3
4
5
6
7
8
9
10
问题来了,我们该如何确定swiper
是上滑了还是下滑了呢?这关系到displayIndex
是+1
还是-1
。细心的朋友应该猜到了,可以通过change
事件的current
参数来判断是上滑还是下滑,如果为0
那就是下滑了,displayIndex
就需要-1
。反之如果current
为2
就是上滑,displayIndex
就需要+1
。
那问题又来了,我们只有3
个swiper-item
,也就是说只能上滑或者下滑1
次,最多两次
。所以我们还需要实现无限滑动
。
要实现无限滑动
,我们就需要在滑动之后修正current
,也就是说,每次上滑或者下滑之后我们需要把current
修正为1
。这样就可以又进行下一次上滑or下滑
首先我们需要分析一下,总共需要处理三种情况,分别是当current
为0,1,2
的时候。我们都需要去更新真实渲染索引的值,也就是displayIndex
的值。
const handleSwiperChange = (e)=>{
const { current } = e?.detail
if(current===0){
// 向上滑动,显示上一个
setDisplayIndex(prev => prev - 1)
}else if(current===1){
}else if(current===2){
// 向下滑动,显示下一个
setDisplayIndex(prev => prev + 1)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
0
和2
的情况比较好处理,我们直接对displayIndex
进行加减操作就行了。1
就比较特殊了,因为当我们修正索引为1
的时候也会触发change
事件,这种情况是不需要处理的,所以在判断中我们要加一个限制条件。只有当displayIndex
为0
并且current
为1
的时候(这时说明用户是从第一个选项卡往下滑),我们才给displayIndex+1
,其余的current为1
的时候我们视为是在修正current
不做处理。
current
为0
和为2
的时候也要考虑一下原始数据首位的边界情况,同样不做处理。然后再更新完displayIndex
的值之后再把swiper
的current
修正为1
const handleSwiperChange = (e)=>{
const { current } = e?.detail
if(current===0 && displayIndex > 1){
// 向上滑动,显示上一个
setDisplayIndex(prev => prev - 1)
}else if(current===1&&displayIndex===0){
setDisplayIndex(1)
}else if(current===2&&displayIndex<originList.length-1){
// 向下滑动,显示下一个
setDisplayIndex(prev => prev + 1)
}
// 重置当前索引为中间位置
setCurrentIndex(1)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里逻辑可能有点绕,大家不要把displayIndex
和current
搞混了哦。current
是swiper
的选项卡的索引,因为我们只渲染三个所以他的值始终在0,1,2
这三个值里切换,那为什么需要displayIndex
呢?因为我们要通过displayIndex
去动态计算出真实渲染的list
的数据,也就是那三个选项卡的数据。
到这里我们就已经实现了一个可以无限滑动,并且只渲染3
个选项卡的swiper
了。如果跟着做的同学应该发现了,虽然我们的swiper
可以无限滑动了。但是有一个问题,在我们修正swiper
的索引的时候也会出现切换动画。这显然是我们不想看到的,所以接下来我们需要做的就是修正索引时让用户无感 我们要悄悄摸摸的替换掉数据
这里我们可以在修正的时候把swiper
的动画时间置为0
,等修正完毕后再改回来。
const [swiperDuration, setSwiperDuration] = useState(300) // 默认动画时间
const handleSwiperChange = (e)=>{
const { current } = e?.detail
// 记录当前索引
setCurrentIndex(current)
if(current===0 && displayIndex > 1){
// 向上滑动,显示上一个
setDisplayIndex(prev => prev - 1)
}else if(current===1&&displayIndex===0){
setDisplayIndex(1)
}else if(current===2&&displayIndex<originList.length-1){
// 向下滑动,显示下一个
setDisplayIndex(prev => prev + 1)
}
// 重置动画时间为0,准备调整位置
setSwiperDuration(0)
// 延迟执行,确保状态更新
setTimeout(() => {
// 重置当前索引为中间位置
setCurrentIndex(1)
// 恢复动画时间
setTimeout(() => {
setSwiperDuration(300)
}, 100)
}, 50)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
同时这里还需要把change
事件换成onAnimationFinish
事件,等切换动画完毕后再修正索引。
是不是以为到此就结束了,NO NO NO!正真的好戏现在才开始。
# 超级版优化
先来回顾一下上面的两种方案:
- 渲染完整数量的
swiper-item
,但只渲染当前swiper-item
的内容 - 只渲染
3
个swiper-item
,通过不断替换数据以及修正current
的方式让swiper-item
一直处于1
的位置,以此来从交互上骗过用户
极致的性能优化肯定受限就要pass
掉第一种方案,第二种方案显然是更满足我们的要求的,但是实现过于复杂,那有没有一种方案既能渲染少量的swiper-item
,又可以实现的比较简单呢?
话不多说 直接进入正题
思路解析
之前一直都在围绕swiper
的current
做文章,现在我们换一种思路,不去考虑current
的问题。就渲染3
个swiper-item
,下面我们罗列一下需要解决的问题点
- 如何实现无限滑动?
- 答:可以采用
swiper
的首尾衔接模式
- 答:可以采用
- 如何首个
swiper-item
和末尾那个swiper-item
不衔接滑动?- 答:动态设置
circular
属性
- 答:动态设置
- 怎么只在当前
swiper-item
里渲染内容,其他两个swiper-item
不渲染内容?- 答:首先我们需要维护一个内容索引,下文简称
curContentIdx
。这个curContentIdx
就是我们要渲染的数据的索引,在swiper
上滑和下滑的时候需要对这个索引进行对应的更新。每个swiper-item
的索引都是唯一的0,1,2
。当前展示的swiper-item
的索引和curContentIdx
的关系是curContentIdx % 3 = swiperItemIndex
。这样一来就可以确定内容怎么渲染了
- 答:首先我们需要维护一个内容索引,下文简称
有了上文的思路 代码就很简单了
export default function VideoPage() {
const [prevIndex, setPrevIndex] = useState(0) // 记录当前索引
const [curContentIdx, setCurContentIdx] = useState(0) // 当前内容的索引
const [circular, setCircular] = useState(false) // 是否循环播放
const [swiperDuration, setSwiperDuration] = useState(100) // 默认动画时间
const [originList, setOriginList] = useState<any[]>([]) // 原始数据列表
// 添加滑动开始处理函数
const handleSwiperChange = (e) => {
const { current } = e?.detail
let direction = '';
if (prevIndex === 0 && current === 2) {
direction = 'up';
} else if (prevIndex === 2 && current === 0) {
direction = 'down';
} else if (current > prevIndex) {
direction = 'down';
} else if (current < prevIndex) {
direction = 'up';
}
setPrevIndex(current);
if (direction === 'down') {
setCurContentIdx(prev => prev + 1);
} else if (direction === 'up') {
setCurContentIdx(prev => prev - 1);
}
};
// 设置衔接滑动
useEffect(() => {
const isSw = !(curVideoIdx == 0 || curVideoIdx == originList.length - 1)
setCircular(isSw)
}, [curVideoIdx])
return (
<View className='video-page'>
<Swiper
className='video-swiper'
circular={circular}
vertical
current={prevIndex}
onChange={handleSwiperChange}
duration={swiperDuration}
>
{[0, 1, 2].map((video, index) => (
<SwiperItem key={index} className='video-swiper-item'>
<View className='video-container'>
{curVideoIdx % 3 === index && (
<view>渲染内容-----</view>
)}
</View>
</SwiperItem>
))}
</Swiper>
</View>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
按照上文的思路去看代码,代码写的很简单。至此我们就完成了一个只有三个swiper-item
,支持首尾数据禁止衔接滑动中间数据可以无限滑动,支持只在当前swiper-item
里渲染内容的高性能swiper
组件。
做到这里就已经大功告成啦!下面来总结一下!
# 总结
本文主要讲述了
swiper
渲染大量数据的优化方案,主要有三种方案
- 不关注
swiper-item
数量,只关注内容(完整渲染swiper-item
,但是之渲染当前的swiper-item
的内容)- 只渲染
3
个swiper-item
,通过不断修正current
和替换渲染数据的方式实现无限轮播,并且只渲染当前swiper-item
的内容- 不从
swiper
本身下手,通过动态变更circular
的方式实现无限轮播和首尾不衔接。通过计算当前内容项索引和swiper-item
索引之间的关系来实现只渲染当前swiper-item
的内容,代码简介,性能高(强推!
)