记一次长列表优化

缘起

理论上不停上拉加载列表,如果不处理,都会随着数据量的增大而变卡。平时的开发中,由于极少会有人上拉那么多数据,一般不会被人发现这个问题。但是就在今天 ,用户反馈我们的电商首页在部分安卓机上巨卡,虽然首页不是我写的,但由于种种原因,最后还是艾特到我了。

背景

Vue写的移动端电商商城

分析

页面卡顿,我第一反应是 dom 节点数量问题。于是在控制台一看:
好家伙,刚进去就有将近7000节点。这下问题应该是锁定了。
看了下页面请求,请求了四个不同的商品列表,每个列表都是全量返回没有分页,梳理了下要实现的需求,发现这个不分页是刻意而为之,于是 长列表优化成了不得不做的事情。

目前的页面结构如下:


<template>
    <some banners />
    <some nav />
    <锚点tab混合组件 />  // 有两个独立tab和两个锚点,两个锚点点击可以定位到任意列表 也可以随页面滚动到合适位置自然激活 这是列表没有分页的主要原因
    <list1 title />
    <list1 v-for />
    <list2 title />
    <list2 v-for />
    <list3 title />
    <list3 v-for />
    <list4 title />
    <list4 v-for />
</template>

每个list item都包裹了厚厚的子节点,列表实际节点数 = 列数 * item子节点数

我们立刻想到,能有效减少 list item 节点数便可以大幅度降低 dom 渲染的开销,达到性能优化的目的

方案

页面组件拿到滚动条高度,传递给子组件,子组件自己判断自己是不是该渲染自身的子节点。


// 页面组件 滚动事件肯定要监听
mounted(){
    window.addEventListener('scroll', this.onScroll)
},
// 务必记得移除
destroy () {
    window.removeEventListener('scroll', this.onScroll)
},
methods:{
    onScroll(){
        // 获取滚动条高度的兼容写法
        this.scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
    }
}


我们知道可视区域高度加上滚动条高度=已经漏过脸的dom高度,如果这个高度大于元素的 offsetTop 则表示元素必须要渲染了

list item component:

<template>
    // 为了撑开容器 固定高度的外层 div 是必须的
    <div>
        <template v-if="showChildNodeList">
            <some nodelist />
        </template>
    </div>
</template>
computed:{
    showChildNodeList(){
        // 加300目的是可以提前300px就渲染 优化体验
        const totalHeight = this.scrollTop + clientHeight + 300
        const calc = totalHeight - this.offsetTop
        return calc > 0 && calc < (clientHeight + 300)  // 大于0表示已经该出来了,小于可视高度+偏移量表示元素又从顶部离开了可视区域可以隐藏起来了
    }
},
mounted() {
    this.offsetTop = this.$el.offsetTop
},

最终效果

滚动不再卡顿,我们检查dom节点:

不管我们如何滚动 即便滚动到页面底部,节点数都在一个可接受范围内

暴力测试

我去除掉优化的代码,故意组装数据把list加到超长,

节点数达到10w+,页面已经卡住不动,没一会儿我电脑都开始抗议(fu~ fu~ fu~ 的散热声

加上优化后的代码:

节点维持在3000左右,滚动依然没什么问题。

检查元素可以发现,不被渲染的地方,只剩下一个 div 壳子

不过也说明一个问题,这种方案并不支持无限滚动的场景,毕竟每增加一列,还是会增加一个div

缺点及优化

虽然当前场景基本解决了,但明显普适性不够,缺点非常明显:

  • 1.每列必须固定高度
  • 2.如果页面因为一些交互导致整个页面高度变了,需要重置每个列的offsetTop
  • 3.并不是真正意义的无限,毕竟每增加一列,还是会增加一个div