[译]无尽滚动的复杂度--来自Google大神的拆解

原文地址:https://developers.google.com/web/updates/2016/07/infinite-scroller
原文作者:Surma
译者:王芃


摘要: 重用你的DOM元素以及删除那些远离可视范围的元素。为延迟显示的元素使用占位符。这里是一个无尽滚动的演示代码

无尽滚动在互联网上到处都有应用。Google Music的艺术家列表是一个,Facebook的时间线是一个,Tweeter的话题列表也是一个。当你向下滚动,新的内容就神奇的“无中生有”了。这是一个得到广泛赞扬的、非常好的用户体验。

在这个无尽滚动背后的技术挑战其实比它看上去要难。当你想做正确的事时,你遇到的问题是巨大的。开始时是一些比较简单的事情,比如在页面尾部的链接是无法点击的,因为内容不断的把它们“挤”走。但是问题逐渐开始变得越来越难:当用户将手机从竖屏改为横屏时你该如何处理 resize 事件?或者当列表过长时你如何避免手机的卡顿?

正确的事

我们认为有充分的理由来实现一个参考设计:在保证性能的基础上,以一个可复用的方式来解决这些问题。

我们将会使用3种技术来达成目标:DOM回收、墓碑和滚动锚定。

我们的demo会是一个类似聊天的窗口,我们可以滚动这些消息列表。首先需要的是一个无尽的消息数据源。从技术角度看,没有任何一个无尽列表是真正无尽的,但当有足够的数据量填充进去时,它们看上去感觉是无尽的。为简化问题,我们这里硬编码了一套消息数据,随机的抽取消息、联系人和图片。为了更像网络的真实情况,我们人为加入了一些延迟。

image_1b8s8bm77scgbh31ill1qn41h199.png-786kB
image_1b8s8bm77scgbh31ill1qn41h199.png-786kB

DOM 回收

DOM回收是一个未被广泛使用的技术,它的用途是让DOM的节点数保持在较低的数值。概括来说,它的机制是利用那些离开视图区域的、已经创建的DOM元素,而不是新建DOM元素。需要承认的一点是DOM节点本身并非耗能大户,但是也不是一点都不消耗性能,每一个节点都会增加一些额外的内存、布局、样式和绘制。如果一个站点的DOM节点过多,在低端设备上会发现明显的变慢,如果没有彻底卡死的话。同样需要注意的一点是,在一个较大的DOM中每一次重新布局或重新应用样式(在节点上增加或删除样式所触发的过程)的系统开销都会比较昂贵。所以进行DOM回收意味着我们会保持DOM节点在一个比较低的数量上,进而加快上面提到的这些处理过程。

第一个障碍是滚动本身。由于我们在任何时刻DOM中只有全部列表项目的一个微小子集,我们需要找到一种方式可以让浏览器正确的反映出理论上应该在“那里”的全部列表项目数量。我们这里用一个 1px * 1px 的”前哨“元素(sentinel),并且应用一个变换使得包含“逃兵”列表项目的元素(下图中的 runway)保持一个理想的高度。我们会把runaway中的每一个元素提升到它们自己的层,保持 runaway 本身是完全空的,没有背景色,神马都木有。如果 runaway层不是空的话,是不利于浏览器优化的。因为我们将不得不在显卡上存储一个由成千上万的像素组成的纹理。这样做显然在移动设备上是不可行的。

当我们进行滚动时,我们会检查是否viewport是否已经足够接近 runaway 的尾部。如果是的话,我们会通过把 sentinel和viewport中的剩余元素移向 runaway的底部来扩展 runaway,然后用新内容渲染这些元素。

向反方向滚动时也类似,但我们无论如何也不会缩小 runaway,原因是我们需要滚动栏的位置保持连续性。

墓碑(Tombstones)

如之前我们所说,我们会尽量让数据源表现的像现实世界遇到的情况:有网络延迟及其它情况。这就意味着如果我们的用户飞快地滚动,他们会很容易就把我们渲染的有数据的项目都甩在身后。如果这种情况发生时,我们就需要放置一个墓碑条目(占位)在对应位置,等到数据取到后墓碑条目会被实际内容替代。墓碑也会被回收,对于墓碑元素会有一个独立的可复用DOM元素的池。这样设计的原因是,我们希望墓碑元素在被实际数据替代时可以有一个漂亮的过渡,而不是出现那种生硬的或者让人迷失的效果。

墓碑元素
墓碑元素

这里有一个有趣的挑战,那就是真实的条目的高度可能会超过墓碑的高度,因为不同的文本量或者图片的大小决定了这点。为了解决这个问题,每次当取到数据后我们会调整当前的滚动位置,而且在viewport之上的一个墓碑条目也会被替换。将滚动位置锚定到某一条目而非某一具体的像素位置,这个概念叫做滚动锚定。

滚动锚定

滚动锚定的触发时机有两个:一个是墓碑被替换时,另一个是窗口大小发生改变时(在设备发生翻转时也会发生)。我们必须要知道在viewport中的最顶部可见元素是什么。由于这个元素可能只是部分可见的,所以我们也需要存储从顶部元素到viewport顶部的偏移量。

滚动锚定
滚动锚定

这样的话,当viewport改变大小时、runaway 改变时,我们是可以把场景恢复到一个看起来和原来几乎一致的样子。爽就一个字!但是改变大小的视窗意味着每个条目都可能改变了高度,那么我们如何能知道该把锚定的内容移动多少偏移量呢?我们并不知道!为了搞清楚这点,我们可能不得不把锚定条目之上的元素布局起来,把它们的高度累加在一起。但显然这样做会造成改变大小时会有明显的停顿,我们并不想要这样的结果。相反,我们借助于一个假设:在viewport之上的每个元素都是和墓碑等高的。根据这个假设来调整对应的滚动位置。当元素滚动进入 runaway 时,我们调整滚动位置,这样就有效的把布局工作延迟到真正需要的时候了。

布局

我刚才跳过了一个重要的细节:布局。每次DOM元素的回收通常情况下都会引发整个 runaway 的重新布局,这会直接影响我们的性能:无法达成每秒60帧的目标。为避免这一点,我们自己承担了布局的重任,使用了绝对定位的元素。这样我们可以让所有 runaway 中的元素感觉上还在占用空间,但其实那里毛都没有。由于我们自己在操控布局,我们便可以缓存每个元素消失前的位置,在用户往回滚动时,我们能立刻从缓存中加载正确的元素。

理想情况下,条目应该只被重绘一次,那就是当它们被加到DOM时。而且应该对于 runaway 中其它条目的增加或删除完全不受影响。这个是可能的,但是只限于现代浏览器。

极致优化

最近,Chrome增加了CSS Containment的支持,这个特性允许开发者告诉浏览器某个元素是布局和绘制的边界。由于我们这里采用的是自己来布局,这是一个很好的可以应用 containment 的机会。当我们增加一个元素到 runaway时,我们知道其它条目不应该被这个重新布局影响。所以每个条目应该设置一个 contain: layout。我们同样也不希望影响站点的其它部分,所以 runaway 本身也需要这样设置。

另一个优化点,我们考虑的是利用IntersectionObservers去检测用户是否已经滚动了足够距离,以便于我们决定是否开始回收DOM和加载新数据。但是 IntersectionObservers 是为高延迟设计的,所以我们实际上会“感觉”用了 IntersectionObservers 反而比不用时“响应更慢”。在我们当前的实现中滚动事件的处理其实也存在这个问题。也许这个问题的可信度较高的解决方案会是 Houdini’s Compositor Worklet

仍不完美

目前的DOM回收实现方式仍不是完美的,因为我们把所有“滚过”viewport的元素都添加到DOM了,而不是仅仅关心那些在屏幕上可见的元素。这就意味着,如果你滚动的真的非常非常快的话,快到你堆积了大量的布局和绘制工作,浏览器已经无法跟上的地步时,这时我们可能除了背景什么都看不到了。这当然不是世界末日但是确实是一个可以优化的地方。

我们希望你可以看到这个过程:当你想提供一个高性能的有良好用户体验的功能时,一个简单的问题是演变成复杂问题的。随着“Progressive Web Apps ”逐渐成为移动设备的一等公民,高性能的良好体验会变得越来越重要,开发者也必须持续的研究使用一些模式来应对性能约束。

所有的代码可以到这里查看,我们已经尽力让代码有可复用性了,但不会发布一个npm类库或其它单独的项目。这个代码的主要目的是教学。

慕课网 Angular 视频课上线: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,629评论 1 92
  • 1. 介绍 浏览器可能是最广泛使用的软件。本书将介绍浏览器的工作原理。我们将看到,当你在地址栏中输入google....
    康斌阅读 1,966评论 7 18
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • 一个人在生活中可能有太多的不顺心和太多的诱惑,乐极生悲和痛不欲生的人我们实在看得太多了。虽然仅二十余岁,但...
    红发香克斯_阅读 168评论 0 0
  • 一 世界上没有无缘无故的爱,也没有无缘无故的恨,一见钟情除外。 我和我老婆的婚姻就是一见钟情的结果。 我第一次遇见...
    小汗哥阅读 227评论 0 0