Taro高性能虚拟列表

Taro高性能虚拟列表

这里记录一种我在Taro上探索出的一种高性能虚拟列表;因为业务需求和Taro框架本身的一些性能问题,在尝试了多种方案之后,找到了一种我认为还算比较高性能的长列表解决方案;

关于长列表渲染的一些方案

  • 直接渲染:最基本的方式,一般适合不复杂的纯文本展示,能承载很大的数据量,但是也不建议使用;
  • 分页渲染:采用上拉加载的方式分页加载数据,这种会随着上拉加载的数据量不断增加出现性能问题;能满足大部分不复杂的展示需求;
  • 虚拟列表:只渲染屏幕"可视区域"的内容,通过占位隐藏不可见区域的内容;实现方式基本都是通过"计算"实现,只是实现计算的方式不一样而已;

先看页面

  • 每个ListItem中的信息还是比较多的
  • 每个卡片都可编辑
  • 同事会展示上百个商品卡片
  • 每个靠还存在大量的判断渲染、交互
  • 卡片高度不固定
    • 卡片高度不固定

遇到的问题

遇到的问题和测试结果都是在Taro 微信小程序中

经过测试发现,该页面同事渲染10个左右的卡片就能看收到明显的卡顿问题;

  • 普通的分页加载上拉比较多的数据的时候就卡了;
  • 二位数组方式,代码逻辑比较复杂,而且对与快速滑动和一键回到顶部不太友好;
  • 官方提供的方案只适用于卡片高度固定的情况;

createIntersectionObserver

developer.mozilla.org/zh-CN/docs/...

可以监听元素和另个元素是否相交,我们可以通过ListItem与屏幕容器是否相交来判断卡片是否需要显示;

上代码

jsx 复制代码
import { ReactNode, useEffect, useRef, useState } from "react";
import { View } from "@tarojs/components";
import { createIntersectionObserver, pxTransform } from "@tarojs/taro";
import debounce from 'lodash/debounce';

interface IVirtualListProps {
  /** 数据 */
  dataSource: {
    /** 唯一标识 使用该标识记录渲染到元素块 */
    id: string
    [key: string]: any
  }[]
  renderListitem: (data) => ReactNode
  listItemHeight: (data: any) => number | number // 这里是为了实现卡片高度不固定的情况,同时保证获取卡片高度,用来占位使用,保证占位元素高度和数据渲染后一致,这样页面就不会抖动了
}

export default function VirtualList({ dataSource, renderListitem, listItemHeight }: IVirtualListProps) {
  const lastItemRef = useRef<any>() // 最后一个item的ref,用来标记是否渲染完成,不然监听不到  
  const [showData, setShowData] = useState<{
    [key: string]: {
      show: boolean
      height: number // 本来是想缓存高度的,这里没有,有优化空间
    }
  }>({})
  const showDataRef = useRef<any>({});
  const intersectionObserverRef = useRef<any>() // 只有一个对象,开始针对每个卡片设置一个对象,很占用cpu,时间久了手机会发烫,现在不会了 

  const updateShow = () => {
    setShowData({ ...showDataRef.current })
  }
  
  const debounceUpdateShow = debounce(updateShow, 200); function observe() {
    // console.log("ref.current", lastItemRef.current, typeof lastItemRef.current)    
    if (typeof lastItemRef.current === 'object') { // 最后一个渲染好了再监听      
      // console.log('intersectionObserver')     
      setTimeout(() => {
        intersectionObserverRef.current.relativeToViewport().observe('.observe', (res) => {
          // console.log('observe', res.boundingClientRect.height, res.id, res.intersectionRatio);       
          showDataRef.current[res.id] = { show: res.intersectionRatio > 0, height: res.boundingClientRect.height };          // 增加防抖批量一次更新,这里是为了在快速滑动的时候不停的改变状态的问题        
          debounceUpdateShow()
        })
      }, 100)
    } else {
      setTimeout(() => {
        observe(); // 这里是一个循环,确保整个列表节点渲染完成,不然有些后面渲染的节点会监听不到    
      }, 100)
    }
  }

  useEffect(() => {
    if (dataSource.length) {
      setShowData({})
      showDataRef.current = {}; intersectionObserverRef.current = createIntersectionObserver(this, { observeAll: true }); observe()
    } return () => {
      if (intersectionObserverRef.current) {
        intersectionObserverRef.current.disconnect() //销毁监听对象,不然会创建很多个,手机会发烫    
      }
    }    // 每次数据更新要从新监听 要重新建立对象  
  }, [dataSource])

  const getHeight = (data) => {
    // 获取卡片高度,用来占位使用,保证占位元素高度和数据渲染后一致,这样页面就不会抖动了   
    if (typeof listItemHeight === 'number') {
      return listItemHeight
    }
    return listItemHeight(data)
  }

  const renderItem = (data) => {
    if (showData[`observe_${data.id}`]?.show) {
      return renderListitem(data)
    }
    // 占位图
    return (
      <View style={{
        height: pxTransform(getHeight(data)),
        width: "100%",
        background: "url(https://static.tongliaowang.com/files/tlfiles/images/1707/12/20170712104436322556.jpg) top center no-repeat",
        backgroundSize: "100% 100%"
      }} />
    )
  }

  return (
    <View>
      {
        dataSource.map((item, i) => {
          lastItemRef.current = dataSource && dataSource.length && (i === dataSource.length - 1);
          return (
            <View key={item.goodsCode}
              ref={lastItemRef} style={{ width: '100%', height: pxTransform(getHeight(item)) }}
            >
              <View className='observe' id={`observe_${item.id}`}>
                {renderItem(item)}
              </View>
            </View>
          )
        })
      }
    </View>
  )
}

总结

这里还存在一些其他问题,比如快速滑动的时候会有白屏的情况(展示的全是占位图);高度可以通过缓存优化;能不能结合分解一起使用,一起使用的时候后来追加的元素监事件怎么解决;

无论列表渲染还是其他情况的渲染性能问题,其实都是因为同一时间渲染到更新的内容过多,我们要做的就是怎么渲染的节点树;

本来想放上视频效果呢,文档不支持视屏,gif效果还原度又不好;

相关推荐
福兮说9 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理10 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆66611 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)11 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan11 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_12 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒12 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技12 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu13 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙14 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript