V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
Kinnikuman
V2EX  ›  程序员

关于"好奇移动端、桌面端是怎么实现列表控件渲染大量元素不卡顿的?"引申的问题

  •  
  •   Kinnikuman · 2024-07-24 10:06:18 +08:00 · 3511 次点击
    这是一个创建于 405 天前的主题,其中的信息可能已经有所发展或是发生改变。

    上一个问题( https://www.v2ex.com/t/1059281 )我看已经讨论结束了,所以新开一个帖子来讨论下。

    我的问题是,大量的列表会导致滑动卡顿吗?移动端有"回收,重用,缓存"这种策略,但如果不使用这种策略,而将大量的列表数据加载到内存中,滑动时候会卡顿吗?

    我的理解是它们已经加载到内存中了,滑动只是将其展示出来,缺点是占用内存特别大。

    如果使用了回收策略,只有屏幕展示的那几条列表会被加载到内存中,滑动出去的放到复用列表中,以供下次使用,这样可以节约大量内存,但在快速滚动刷新的列表中,这需要 cpu 进行大量的计算来刷新列表中的数据吧?

    所以我觉得,如果不使用回收策略,那么 cpu 会在第一时间创建很多列表数据,这会导致一开始卡顿,创建完数据后,占用很大内存。但之后的刷新,不应该卡顿。

    如果使用回收策略,内存压力小了,开始不需要进行大量的 cpu 计算,所以不会有开始渲染卡顿问题。但后面的快速刷新会消耗 cpu 。

    第 1 条附言  ·  2024-07-24 10:47:25 +08:00
    大家都在讲渲染的问题。渲染不是系统框架做的工作吗?程序能够控制的,是配合这些框架来优化性能,比如使用某个快速的算法来计算每一个列表的高度,或者是固定高度,或者是简单的计算高度。

    所以我的问题再优化下,对于回收策略来讲,复杂的列表,在快速滑动情况下,这样的 cpu 工作(将数据分配到可复用的列表中)也是很轻松的,已经没有可优化的空间了。对吗?
    29 条回复    2024-07-25 10:01:58 +08:00
    LuckyLauncher
        1
    LuckyLauncher  
       2024-07-24 10:18:09 +08:00   ❤️ 1
    数据是数据,展示出来是“绘制”这个动作
    你把你的手机屏幕上展示的东西想象成一张静态的图,你滑动的时候这张图是被实时绘制出来的,不管数据在哪,要渲染的组件有没有创建(哪怕是已经创建了但是给隐藏了),“绘制”这个动作是跑不了的

    你可以用 canvas 之类的自己写一个列表看看
    jones2000
        2
    jones2000  
       2024-07-24 10:22:58 +08:00
    虚拟表格不需要把所有数据都加载到内存, 只加载一个索引序号就可以了,滚到到哪里就请求哪一屏的数据。就算存在内存里面, 能用多少内存, 比起创建 10W 个 DOM 占用的内存,100W 条数据内存就小多了。
    IvanLi127
        3
    IvanLi127  
       2024-07-24 10:25:49 +08:00
    CPU: 谢谢你
    GPU: 我谢谢你
    RightHand
        4
    RightHand  
       2024-07-24 10:27:26 +08:00 via Android
    单纯数据其实还好,绘制才是卡的大头。
    weixind
        5
    weixind  
       2024-07-24 10:30:17 +08:00
    这个场景的瓶颈首先是在渲染,而不是数据源。有定论的知识点,有啥可讨论的。
    iOCZS
        6
    iOCZS  
       2024-07-24 10:32:01 +08:00
    绘制的消耗是必然存在的,在绘制间隙的 CPU 时间片可以用于计算,这是合理的,CPU 闲着也是闲着。
    不使用回收策略,那么 cpu 会在第一时间创建很多列表数据。这个有待商榷,一般是分页获取数据,因此内存消耗是累计的,CPU 未必存在一开始就卡的问题。
    Kinnikuman
        7
    Kinnikuman  
    OP
       2024-07-24 10:32:51 +08:00
    @LuckyLauncher

    @RightHand

    绘制是另一个问题了,有专门的硬件处理。

    所以,我的问题有问题是吗?相对于绘制来讲,那些 cpu 计算复杂的列表如何更换数据,都是很 easy 的计算?
    Kinnikuman
        8
    Kinnikuman  
    OP
       2024-07-24 10:35:19 +08:00
    @iOCZS "CPU 闲着也是闲着",不这么认为,能让 cpu 少工作,也算是性能优化的一部分吧。当然这个话题中是舍弃掉内存来换取 cpu 的部分工作。而且这个讨论不考虑分页获取数据,就是一个几万几十万条的数据一次性加载到 list 中。
    Chuckle
        9
    Chuckle  
       2024-07-24 10:36:19 +08:00   ❤️ 3
    虚拟列表是在滚动时计算出要渲染的元素在数组中的索引范围,普通的定高、不定高的计算量不大,很流畅,但是不定高的瀑布流,还伴随着图片加载的话,计算量就很大了,写过个 demo ,https://list.qcqx.cn/#/list/virtualwaterfall
    Chuckle
        10
    Chuckle  
       2024-07-24 10:38:16 +08:00
    @Chuckle 后端把图片宽高返回的话,计算量能小点,小红书就是这么干的
    ipwx
        11
    ipwx  
       2024-07-24 10:52:35 +08:00
    在桌面 UI 时代,有一个东西叫做 onDraw (clipRect):UI 框架告诉你,现在你这个控件需要显示 (x0,y0) -> (x1,y1) 区域的东西,你自己画吧。

    所以你根本不需要构造一堆 DOM 元素。列表在你的内存里面仅仅是数据,比如 List[{name: Steven, age: 13, ...}],然后你自己先把每个列表项渲染出来的坐标范围给计算出来存着,然后根据 UI 的需求把显示出来的对象画出来就可以了。

    而且如果你遍历一遍你的列表找 (x0,y0) -> (x1,y1) 范围内的元素慢(这是 O(n) 的操作),你可以上数据结构,比如线段树,然后你就 O(log n) 了。

    用上这套优化,百万个元素也不在话下。毕竟内存里面放一百万个对象才多少,也就几百兆么(注意 1 兆 = 一百万字节)。
    ----

    题外话,所以很多前端不理解 “干嘛老考数据结构和算法”,那是因为没遇上需求。。。
    ipwx
        12
    ipwx  
       2024-07-24 10:54:46 +08:00
    另外吐槽一句,上古时代 onDraw 要写的东西太多了以至于是大神才能玩的。

    后来各大 UI 框架都有了它们自己的绘图的框架,降低了这套东西的难度。我学得少,只知道一个 Qt 的 GraphicsView 干这事,还有 JS 可能有一些 Canvas 的库干这些活。其他就不知道了。
    Chuckle
        13
    Chuckle  
       2024-07-24 11:04:08 +08:00
    @Chuckle #9 拟列表一般都是滑到底部后增量加载,类似分页,并不是一次性把所有数据加进 list ,而且计算布局也限制在视口附近的元素,优化手段还是很多的,查找要渲染的元素范围用二分,当然,往下滑动多了,list 还是会很大,可以考虑分数组、按范围计算,甚至上 canvas ,不过一般来说那点数据量 cpu 应付得过来的,总比上万个 dom 元素好多了,至于内存占用,这个没特殊限制倒没大问题,100w 个对象也才多大,重点还是列表布局的渲染,数据量大了怎么搞都是妥协,布局还是得老老实实算。这 demo 写得也一般,但是不定高虚拟瀑布流也能应付无图片的上万条数据。
    Skifary
        14
    Skifary  
       2024-07-24 11:29:48 +08:00   ❤️ 1
    "所以我觉得,如果不使用回收策略,那么 cpu 会在第一时间创建很多列表数据,这会导致一开始卡顿,创建完数据后,占用很大内存。但之后的刷新,不应该卡顿。

    如果使用回收策略,内存压力小了,开始不需要进行大量的 cpu 计算,所以不会有开始渲染卡顿问题。但后面的快速刷新会消耗 cpu 。"

    ------------------------------------------------

    1 ,你没有理解 UI 对象,任何框架的 UI 对象都有绘制函数,而绘制函数是每一帧都会执行的,不管 UI 对象是否处于可视范围内,这是大数据列表卡顿最根本的原因。

    2 ,使用缓存只会创造固定数目的 UI 对象,那么无论多大的数据量,最终的需要绘制的对象永远只有那几个,不会随着数据量的增长而增长。

    3 ,对比“后面的快速刷新会消耗 cpu”需要的 cpu 算力和“渲染成千上万 UI 对象”所需要的 cpu 算力相比就是九牛一毛。
    crz
        15
    crz  
       2024-07-24 13:09:09 +08:00
    @Chuckle 不定高的问题是滚动条,普通元素的滚动条是和内容线性对应的,虚拟列表要手动对齐这部分交互体验。

    以你的 demo 为例子,不定高度一页是 2000 条数据,ctrl+end 定位的话尾部应该是第 2000 条,我这里操作结果是 1946 。再一个是拖动滚动条,能明显看到鼠标指针和滚动条慢慢错位。

    滚动条拉到一半位置,定高只要按索引折半,再细点就算上容器高度,简单的计算。不定高就不一样了。

    碰到不定高度的直接想法就是算出来缓存,后台绘制,缓存。问题是虚拟列表数量往往不会少,就算只绘制一遍也是不小的开销,要是再加上加载图片的场景,时间和精确至少少一个。

    后端给出高度也是一个方案,不考虑后端如何得到数据的问题,前端的一个问题场景是不定宽度的容器,数据高度受宽度影响也会变化。

    不考虑滚动条的事行不行?也可以,虽然体验有区别,细节处大部分人没碰到也不会在意,也许以后大家都默认虚拟列表的交互就是这样的
    kera0a
        16
    kera0a  
       2024-07-24 13:17:35 +08:00 via iPhone
    生成这么多元素,就算不绘制只去判断元素是否要绘制都是一笔很大的开销,列表每滚动一帧就得来一次批量判断,计算元素在不在显示范围
    archxm
        17
    archxm  
       2024-07-24 13:35:46 +08:00
    有优化手段的,做过 duilib
    xu33
        18
    xu33  
       2024-07-24 15:23:59 +08:00
    这个主要是渲染问题,一般数据全部放内存够了,数据如果实在大,可以考虑持久化存储再 load ,内存里只放一个 id
    unco020511
        19
    unco020511  
       2024-07-24 16:37:17 +08:00
    你还是没理解
    DOLLOR
        20
    DOLLOR  
       2024-07-24 18:02:27 +08:00
    不是应该提供分页、跳页和条件筛选吗?
    这么大量的数据,即使 UI 不卡,要找到想要的数据也不方便,用户体验也不会好。
    mcluyu
        21
    mcluyu  
       2024-07-24 18:07:37 +08:00
    不触发离屏渲染的前提下, 渲染问题解决了, 但是上面提到了,即便是不停的判断元素在不在屏幕范围内也是不小的开销
    superkeke
        22
    superkeke  
       2024-07-24 18:17:44 +08:00
    移动端性能优化,早年面试必谈的问题,这个点就太多了,能做的点也很多。数据重用、异步绘制、布局优化、预加载,还可以涉及到操作系统层面,太多了。
    Chuckle
        23
    Chuckle  
       2024-07-24 19:44:21 +08:00
    @crz 原生的滚动条确实应该隐藏掉,要的话应该再写个虚拟滚动条,小红书也是把滚动条隐藏了,demo 不直接展示 2000 条,也是因为实时计算是找最小高度的列,以其为基准,所以肯定是少于 2000 条的,确保体验,不然有些列长度太短了,留白难看,因为是不定高,所以每次加入元素只能找最短列,但不知道当前加入的元素实际高度。
    Chuckle
        24
    Chuckle  
       2024-07-24 19:46:11 +08:00
    @DOLLOR 主要还是小红书、抖音这种无限往下滑动的场景,快速找到并滚动到最后一次看的视频也是个算法题(
    2I0Mto2kjm0c0B9i
        25
    2I0Mto2kjm0c0B9i  
       2024-07-24 19:51:12 +08:00
    @Chuckle 请教下你这个能处理带日期分类的那个虚拟滚动吗? 1 行是某天日期下一行是该天所有的照片,照片是横向排列的一行摆不下就自动换行
    Chuckle
        26
    Chuckle  
       2024-07-24 22:36:47 +08:00
    @iapplebear 每个列表元素的 dom 结构可以通过插槽自定义,你可以通过二维数组实现这个功能,外面是一层不定高虚拟列表,用于区分每一天,然后每个元素里面又是一个不定高虚拟瀑布流来展示该天的所有照片,通过这样嵌套两层虚拟列表,应该可以满足你的需求。
    Chuckle
        27
    Chuckle  
       2024-07-24 22:41:23 +08:00
    @iapplebear 不过这毕竟只是个 demo ,我感觉嵌套起来用应该会有些问题,性能上或者是布局计算上
    ko1haha
        28
    ko1haha  
       2024-07-24 23:29:19 +08:00
    虚表控件十分重要,尤其是移动设备上,重要程度仅次于 webview 。不像 PC 你可以仗着电脑性能好随便写。

    怎么分页也是一种策略,包括界面和数据的分页。

    有的分页,需要手动翻页。有的虽然不需要,但是加载分页的速度很慢。。


    c++虚表也不难,但是优化比较难。(我就写过,模仿安卓的 ListView 扩展 duilib ,几百行代码,就把这些试了一遍:item 不同高度,按行滚动 vs 按像素滚动,平滑滚动动画)

    体验最好的当数浏览器。然而浏览器其实也没有原生的虚表控件,需要用库或者自己写。

    小红书网站确实是虚表,然而那体验实在太差了,和抖音不能比。

    > @Chuckle 快速找到并滚动到最后一次看的视频也是个算法题

    p 的算法题,主页视频就算千万个,直接 for 循环查找下去也是很快的,不要低估 CPU 的运行速度。(我就写过用户脚本,把小红书变成抖音模式,近似)

    ---

    基于浏览器做个文件管理器,取代老古董 explorer:就类似书签管理器的那套分页 UI ,运用到文件管理里,加载图片也是可以的。直接复用框选多选等逻辑了,爽。

    浏览器最棒的是 css ,可以各种粉饰,轻松转换网格和列表布局。

    说一下我的分页方法:先分块,再分页

    分三个块,滚动到末尾的时候,搬动块。

    每个块又分十个页面,每个页面管 30 行。约定一个最小行高度,视口外的页面不含 dom 元素,只有一个最小的高度。然后滚动的时候,触发 bind 函数,渲染视口内的页面。

    瑕疵:1. 原生滚动条的位置不准。2. 网格模式下,会有空缺。

    优点:1. 降低渲染压力。2. 恢复列表位置
    2I0Mto2kjm0c0B9i
        29
    2I0Mto2kjm0c0B9i  
       2024-07-25 10:01:58 +08:00
    @Chuckle 谢谢,不过我这边是 React ,还有个难点是要支持浏览器 resize 之后重排,照片的比例不变宽高是动态变化的
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   930 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 21:54 · PVG 05:54 · LAX 14:54 · JFK 17:54
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.