惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
量子位
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
B
Blog
U
Unit 42
C
Check Point Blog
I
InfoQ
aimingoo的专栏
aimingoo的专栏
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
爱范儿
爱范儿

蚊子的前端博客

微说 | 有的人觉得只要不考XX,一定能考好!-蚊子的前端博客 微说 | 春天来了-蚊子的前端博客 明天和意外不知道哪个先来-蚊子的前端博客 微说 | 既然错过了路口,就及时止损,重新规划路线上路-蚊子的前端博客 微说 | 2025年的出生人口数是792万-蚊子的前端博客 微说 | 工作年终总结,让写对接的接口的数量?-蚊子的前端博客 微说 | 温家宝:没有政治体制改革的成功 经济体制改革不可能进行到底-蚊子的前端博客 微说 | 违法犯罪了该不该被禁言?-蚊子的前端博客 微说 | 不明白为什么在推荐去俄罗斯旅游-蚊子的前端博客 微说 | 易中天论骗子-蚊子的前端博客 微说 | 一场大风吹散了秋天-蚊子的前端博客 微说 | 既要、又要、还要、更要!-蚊子的前端博客 又是一年的国庆雨季-蚊子的前端博客 微说 | 预估下2025年的出生人口数据-蚊子的前端博客 微说 | 可惜了我那些小时候的书本-蚊子的前端博客 微说 | 牛马有的是,驴不够了!-蚊子的前端博客 微说 | 能否有一条非户口也能高考的路-蚊子的前端博客 微说 | 小聊中医-蚊子的前端博客 微说 | 将要制作的一款新产品-蚊子的前端博客 微说 | 做人要有信-蚊子的前端博客 微说 | 好一个正义联盟-蚊子的前端博客 微说 | 都是见过吃过的主儿-蚊子的前端博客 微说 | 追求8小时工作制有错吗?-蚊子的前端博客 微说 | 如果尖锐的批评完全消失-蚊子的前端博客 微说 | 很好!-蚊子的前端博客 微说 | 封禁用户可以,但要告知具体原因-蚊子的前端博客 微说 | 面朝大海,春暖花开-蚊子的前端博客 微说 | 程序员的悲哀是什么?-蚊子的前端博客 微说 | 2025年出生人口的预测-蚊子的前端博客 前端在 LiveKit 中如何获取所有的参与者-蚊子的前端博客
页面中长列表滚动的优化-蚊子的前端博客
author · 2019-04-23 · via 蚊子的前端博客

当我们有 10 万条或者更多的数据需要展示在页面中,我们应该怎样处理呢?

这里主要的方式有两种:

  1. 懒加载:即监听 scroll 事件或使用IntersecionObserver监听;
  2. 可视区域的渲染:仅在可视区域展示数据,为保证滚动条的完整性,非可视区域使用占位元素的高度后者容器的位移来撑开。

1. 懒加载 #

懒加载的方式有两种:监听 scroll 事件或使用IntersecionObserver监听某个元素。

1.1 scroll 事件 #

监听滚动事件应该是我们使用的最多的事件了,当滚动条滑动到页面最底部或者将要滑动到最底部的时候,去加载下一页的数据。同时图片懒加载或者其他组件的懒加载也可以依赖于滚动事件!

为了优化滚动事件,我们也会给滚动事件添加上防抖和节流

这是一个很常见的 demo,无限加载数据系列,当滚动条滑动到底部时,加载下一页的数据:监听滚动事件懒加载数据

请分别用微信和 QQ 扫下这个二维码体验一下,体验时,请点击“向左”的按钮关闭代码的部分:

监听滚动事件懒加载数据-蚊子博客

其实我们发现会有一个很有意思的体验:在 android 中的微信和 QQ 中基本可以无限的滑动,在 iOS 的微信中也可以,但是在 iOS 的 QQ 中,当我们一直滑动时,需要滑动到底部,然后才能加载新的数据,才能继续滑动!

这种原因是 iOS 不同的 webview 造成的。在 iOS 中,UIWebView 是在 iOS2 中出现的,性能更好的 WKWebView 跟随 iOS8 一起出现。从性能方面来说,WKWebView 会比 UIWebView 高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的 H5 页面支持,类比 UIWebView 还多提供了一个加载进度的属性。同时,在 UIWebView 中,无论是页面的 body 滚动,还是 div 级别的局部滚动,都不会实时触发,而是在滚动结束后才会触发!

可以分别体验下面的两个二维码,左侧的为 body 级别的滚动,右侧为 div 的局部滚动:

body级别的滚动-蚊子博客 div的局部滚动-蚊子博客

总结下滚动事件在不同的 webview 中的表现情况:

机型 body 滚动 局部滚动
iOS WKWebView 实时触发 实时触发
iOS UIWebView 无法实时触发 无法实时触发
Android 实时触发 实时触发

那么该如何优化滚动的性能呢?

首先就是防抖和节流。在 Chrome 浏览器中使用performance.now()测试滚动每次触发的间隔,可以看到,频率大概在 16ms 左右,但很多时候我们并不需要这么快地触发我们的监听函数,而且,比如图片懒加载等场景,也要及时的更新一次,这时候就用到了防抖和节流!

再有就是使用requestAnimationFramerequestIdleCallback代替定时器。若使用定时器进行间歇性触发监听函数,会出现掉帧的情况,我们也知道setTimeout(fn, 100),不一定是正好在 100ms 后触发 fn 函数,而是等浏览器空闲之后才调用 fn 函数,当多次积累后就会产生掉帧的,那么使用requestAnimationFramerequestIdleCallback,则浏览器会根据自己渲染的频率适时地执行回调。在追求高性能的渲染效果时,可以考虑用 requestIdleCallback()和 requestAnimationFrame()代替定时器。前者适合流畅的动画效果场景,后者适用于分离一些优先级低的操作逻辑,使用时需要考虑清楚

1.2 IntersecionObserver #

使用 IntersecionObserver 也可以实现无限滚动,比如在底部监听一个透明的元素,当该元素可见时就加载新的资源!

demo: IntersecionObserver 实现无限滚动

不过也应当看到的是 IntersecionObserver 的支持程度还不太好,如果要使用的话,还是需要 polyfill 方案!

IntersecionObserver的浏览器支持程度

而且 IntersecionObserver 在图片懒加载和组件懒加载的过程中,非常有用,我们后续会进行了解!

2. 模拟滚动 #

模拟滚动最代表的例子就是iScroll,监听手势的 touchmove 事件,然后使用 CSS3 中的 transform 产生位移。

不过在模拟滚动的过程中,若图片比较多时,可以感觉到滚动时有明显的卡顿感。查看这个样例:https://www.xiabingbao.com/demos/20190423/iscroll-scroll.html

这是利用 IScroll 中的scroll事件,无限地向页面中添加元素。不过当页面的数据是无限加载时,应当使用iscroll-infinite这个类库更好,这部分留到后面讲解!

3. 可视区域数据的渲染 #

上面的几种方法里,我们都是无限地直接往页面中添加元素,滑动的页码越大,页面中的 DOM 元素也就越多。但实际上,这是没有必要的,滚动到可视区域外的元素,用户是看不到的,也没不要保留在页面中,可以用一个占位元素或者 transform 撑开上面所有不可见元素的高度,让滚动条能够正常的上下滑动即可!

撑开上半部分不可见区域的高度,有两种方法,一种是在容器内的最顶部设置一个占位元素,这个占位元素的高度就是消失的所有 DOM 的高度;再一个就是给容器或者占位元素一个 transform 的位移!这里我们使用给占位元素一个高度撑起顶部的滚动区域。

3.1 固定高度的 item #

若每个 item 的 height/margin/padding 等数据都是固定写死的,这种情况比较容易实现!获取一次数据后,不用每次都重新计算。

每次滚动时,都要计算应当渲染列表的哪部分!start表示列表开始的位置,fixedScrollTop表示顶部占位元素的距离

// 滚动处理函数
function handleScroller() {
  let lastStart = 0; // 上次开始的位置
  const item = document.querySelector('.container .item');
  const itemStyle = getComputedStyle(item);
  const itemHeight = item.offsetHeight + parseInt(itemStyle['marginTop']) + parseInt(itemStyle['marginBottom']);

  return function () {
    const currentScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
    const fixedScrollTop = currentScrollTop - (currentScrollTop % itemHeight);
    let start = Math.floor(currentScrollTop / itemHeight); // 可视区域开始渲染的位置

    if (lastStart !== start) {
      lastStart = start;
      createDom(start, count, fixedScrollTop);
    }
  };
}

设置顶部滚动的高度:

function createDom(start, count, height) {
  const container = document.querySelector('.container');
  // container.style.transform = `translateY(${height}px)`; // 给容易一个位移

  let div = document.createDocumentFragment();

  // 创建占位元素
  if (height) {
    let p = document.createElement('p');
    p.style.height = height + 'px';
    div.appendChild(p);
  }

  for (let i = start, len = start + count; i < len; i++) {
    let item = document.createElement('div');
    item.className = 'item';
    item.innerHTML = i;
    div.appendChild(item);
  }

  // 为了方便处理,我们这里采用了更新container中的全部元素
  // 你也可以尝试只增加/删除首位的元素,中间元素不变
  container.innerHTML = '';
  container.appendChild(div);
}

可以点击链接查看样例:可视区域渲染之 DOM 元素增减,审查元素可以看得更清晰,无论我们怎么滚动,.container中永远只有那么几个元素!

3.2 每个 item 的高度都不一定 #

待定,有个问题一直搞不定

3.3 DOM 的复用 #

重点来了,在上面的章节里,我们进行了进一步的优化,只渲染可视区域内的数据。但是还是存在一个重要的问题:频繁的改动 DOM,这样会频繁的引起页面的重绘。这里我们的思想是对页面中 DOM 元素进行复用,如下图中所示,从上面滑出的元素,可以直接定位下面再重新装填元素,反之亦然!

DOM元素的复用-蚊子博客-蚊子博客

可以点击链接查看 demo,在审查元素中我们可以看到,在滚动的过程中,DOM 并没有被删掉,而是改变了 transform,放到了最下面:可视区域渲染之 DOM 元素复用

// 更新页面中DOM元素的位置
function updateDom(start, count, itemHeight, height) {
  document.querySelector('.container .content').style.transform = 'translateY(' + height + 'px)';
  for (var i = start, len = start + count; i < len; i++) {
    var index = i % count;
    var cssIndex = (i - start) % len;
    document.querySelector('.item' + index).innerHTML = i;
    document.querySelector('.item' + index).style.transform = 'translateY(' + itemHeight * cssIndex + 'px)';
  }
}

这里我们还是监听了元素的 scroll 事件,我们在上面一笔带过的iScroll-infinite类库,是使用了模拟滚动+DOM 复用。点击链接查看 demo:iScroll-infinite 实现的无限加载

4. 总结 #

最有效的方案应当是模拟滚动+DOM 复用,既解决了滚动不能实时生效的问题,又能解决页面中 DOM 元素过多,造成滚动中卡顿的问题。

总是想着要讲的有很多,不过最后好像哪方面都欠缺了一点,后面有机会再针对其中的某一个点再深入进行探讨!例如使用IntersecionObserver实现 Vue 组件的懒加载,如何自己实现一个简单的模拟滚动等。

参考: