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效果还原度又不好;

相关推荐
顾尘眠2 小时前
http常用状态码(204,304, 404, 504,502)含义
前端
王先生技术栈4 小时前
思维导图,Android版本实现
java·前端
悠悠:)4 小时前
前端 动图方案
前端
星陈~4 小时前
检测electron打包文件 app.asar
前端·vue.js·electron
Aatroox4 小时前
基于 Nuxt3 + Obsidian 搭建个人博客
前端·node.js
每天都要进步哦5 小时前
Node.js中的fs模块:文件与目录操作(写入、读取、复制、移动、删除、重命名等)
前端·javascript·node.js
brzhang6 小时前
开源了一个 Super Copy Coder ,0 成本实现视觉搞转提示词,效率炸裂
前端·人工智能
diaobusi-886 小时前
HTML5-标签
前端·html·html5
我命由我123456 小时前
CesiumJS 案例 P34:场景视图(3D 视图、2D 视图)
前端·javascript·3d·前端框架·html·html5·js
就是蠢啊6 小时前
封装/前线修饰符/Idea项目结构/package/impore
java·服务器·前端