乐趣区

关于javascript:前端多数据渲染优化

前言

在前一段时间做一个需要的时候, 碰到一个自定义列表的性能, 他的所有数据显示都是通过 jSON 字符串来存储, 应用也是通过 JSON 解析 起先他是有数据下限的, 然而前面进步下限后就呈现了卡顿等问题,
所以本文就是介绍一些计划来解决前端大量数据的渲染问题

计划

innerHTML

首先是在很久很久之前的渲染计划 innerHTML 插入, 他是官网的 API, 性能较好

这是一个简略的 HTML 渲染例子(在试验时数据取 10w 级别, 扩充差别, 理论中根本会小于这个级别)

    const items = new Array(100000).fill(0).map((it, index) => {return `<div>item ${index}</div>`
}).join('')
content.innerHTML = items

来自谷歌的性能剖析:

在 10 秒内进行了页面的刷新和滚动, 能够看到 dom 的渲染阻塞了页面 1300 ms

在性能检测中, 总阻塞工夫管制在 300 毫秒 以内才是一个合格的状态, 这个工夫还会受电脑硬件的影响

总结下这个办法的优缺点:

  • 长处: 性能绝对能够承受, 但数据较多时也同样有阻塞
  • 毛病:

    • 有注入的危险, 和框架的搭配较差
    • 在 dom 过多时并没有解决滚动的性能问题

批量插入

通过分片来插入, 如果有 10W 条数据, 咱们就分成 10 次, 每次 1w 条循环插入

    [...new Array(10)].forEach((_, i) => {requestAnimationFrame(() => {[...new Array(10000)].forEach((_, index) => {const item = document.createElement("div")
            item.textContent = `item ${i}${index}`
            content.append(item)
        })
    })
})

通过谷歌剖析:

这里也是包含的页面刷新和滚动的性能剖析, 能够看到阻塞工夫为 1800 毫秒, 相较 innerHTML 来说会差一点, 这是在 10w 的这个数量级, 数量越小, 工夫的差距也会越小

对于 requestAnimationFrame

其中 requestAnimationFrame 的作用: 此办法会通知浏览器心愿执行动画并申请浏览器在下一次重绘之前调用回调函数来更新动画。

执行形式: 当执行 requestAnimationFrame(callback)的时候,不会立刻调用 callback 回调函数,会将其放入回调函数队列,
当页面可见并且动画帧申请 callback 回调函数列表不为空时,浏览器会定期将这些回调函数退出到浏览器 UI 线程的队列中(由零碎来决定回调函数的执行机会)

总的来说就是不会阻塞其余代码的执行, 然而总的执行工夫和 innerHTML 计划差不太多

总结下优缺点:

  • 长处: 不会阻塞代码的运行
  • 毛病:

    • 插入所破费的总工夫仍旧和 innerHTML 差不太多
    • 同样地, 在 dom 过多时也没有解决滚动的性能问题

其余原生形式

canvas

canvas 是专门用来绘制的一个工具, 能够用于动画、游戏画面、数据可视化、图片编辑以及实时视频解决等方面。

最近在驰名框架 Flutter 的 Web 中就是应用 canvas 来渲染页面的

同样咱们也能够应用 canvas 来渲染大量的数据


<div style="max-height: 256px;max-width:256px;overflow: scroll;">
    <canvas id="canvas"></canvas>
</div>
    let ctx = canvas.getContext('2d');
[...new Array(100000)].map((it, index) => {ctx.fillText(`item ${index}`, 0, index * 30)
})

通过理论的尝试, canvas 他是有限度的, 最大到 6w 左右的高度就不能再持续放大了, 也就是说在大量数据下, canvas 还是被限制住了

进一步优化

这里提供一个优化思路, 监听外层 DOM 的滚动, 依据高度来动静渲染 canvas 的显示, 能达到最终的成果, 然而这样老本还是太高了

  • 长处: 在渲染数量上性能很好
  • 毛病:

    • 想要实现虚构列表一样的渲染, 不可控(在其余场景下是一种比拟好的计划, 比方动画, 地图等)
    • 在 canvas 中的款式难以把控

IntersectionObserver

IntersectionObserver 提供了一种异步察看指标元素与视口的穿插状态,简略地说就是能监听到某个元素是否会被咱们看到,当咱们看到这个元素时,能够执行一些回调函数来解决某些事务。

留神:
IntersectionObserver 的实现,应该采纳 requestIdleCallback(),即只有线程闲暇下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其余工作执行完,浏览器有了闲暇才会执行。

通过这个 api 咱们能够做一些尝试, 来实现相似虚构列表的计划

这里我实现了往下滑动的一个虚构列表 demo, 次要思路是监听列表中所有的 dom, 当他隐没的时候, 移除并去除监听, 而后增加新的 DOM 和监听


外围代码:

    const intersectionObserver = new IntersectionObserver(function (entries) {
        entries.forEach(item => {
            // 0 示意隐没
            if (item.intersectionRatio === 0) {
                // 最初开端增加
                intersectionObserver.unobserve(item.target)
                item.target.remove()
                addDom()}
        })
    });

谷歌的性能剖析(首次进入页面和继续滚动 1000 个 item):

能够看到根本是没有阻塞的, 此计划是可行的, 在初始渲染和滚动之间都没问题

详情点击能够查看, demo 只实现了往下滚动计划:
https://codesandbox.io/s/snow…

进一步优化

当初 IntersectionObserver 曾经实现了相似虚构列表的性能了, 然而频繁的增加监听和解除, 怎么都看起来会有隐患, 所以我打算采取扩大化的计划:

大略的思路:

以后列表以 10 个为一队, 以后列表总共渲染 30 个, 当滚动到第 20 个时, 触发事件, 加载第 30-40 个, 同时删除 0 -10 个, 前面顺次触发

这样的话触发次数和监听次数会呈倍数降落, 当然代价就是共事渲染的 dom 数量减少, 后续咱们再度减少每一队的数量, 能够维持一个
dom 数和监听较为均衡的状态

兼容

对于 IntersectionObserver 的兼容, 通过 polyfill, 可取得大多浏览器的兼容, 最低反对 IE7, 具体可查看: https://github.com/w3c/Inters…

总结下优缺点:

  • 长处: 利用原生 API 实现的一种虚构列表计划, 没有数据瓶颈
  • 毛病:

    • 生产中的框架的适配性不够高, 实现较为简单
    • 在有限滚动中频繁触发监听和解除, 可能存在某些问题

框架

后面说了那么多办法, 都是在非框架中的实现, 这里咱们来看一下在 react 中列表的体现

react

这是一个长度为 1 万 的列表渲染

function App() {const [list, setList] = useState([]);

  useEffect(() => {setList([...new Array(50000)]);
  }, []);

  return (
    <div className="App">
      {list.map((item, index) => {return <div key={index}>item {index}</div>;
      })}
    </div>
  );
}

在 demo 运行的时候能够显著地感知到页面地卡顿了

通过谷歌剖析, 在 5 万的数量级下, 从新刷新之后, 10 秒依然没有渲染结束
当然框架中的性能必定是没有原生强的, 这个论断是在预料之内的

在线 demo 地址: https://codesandbox.io/s/angr…

还须要留神的一点是, 大量数据在 template 中的传输问题:

// 这个 list 的数量级是几千甚至上万的, 会导致卡顿成倍的减少, 
<Foo list={list}/>

这个论断不论是在 vue 中还是 react 都是实用的, 所以大量数据的传递, 就得在内存中赋值, 获取, 而不是通过模块,render 等惯例形式

如果数量级在是 100 的, 咱们也能够思考优化, 能够千里之行; 始于足下

startTransition

在 react18 中还会有新的 API startTransition:

startTransition(() => {setList([...new Array(10000)]);
})

这个 API 的作用, 和我下面所说的 requestAnimationFrame 大同小异, 他并不能加强性能, 然而能够防止卡顿, 优先渲染其余的组件, 防止白屏

虚构列表

这里正式引入虚构列表的概念

贮存所有列表元素的地位,只渲染可视区 (viewport)内的列表元素,当可视区滚动时,依据滚动的 offset 大小以及所有列表元素的地位,计算在可视区应该渲染哪些元素。

一张动图看懂原理:

最小实现计划

这里咱们尝试下本人实现一个最小的虚构列表计划:

// 这是一个 react demo, 在 vue 我的项目中, 原理相似, 除了数据源的设置外根本没什么变动

// 数据源以及配置属性
const totalData = [...new Array(10000)].map((item, index)=>({index}))
const total = totalData.length
const itemSize = 30
const maxHeight = 300

function App() {const [list, setList] = useState(() => totalData.slice(0, 20));

  const onScroll = (ev) => {
    const scrollTop = ev.target.scrollTop
    const startIndex = Math.max(Math.floor(scrollTop / itemSize) -5, 0);
    const endIndex = Math.min(startIndex + (maxHeight/itemSize) + 5, total);
    setList(totalData.slice(startIndex, endIndex))
  }

  return (<div onScroll={onScroll} style={{height: maxHeight, overflow: 'auto',}}>
            <div style={{height: total * itemSize, width: '100%', position: 'relative',}}>
              {list.map((item) => {
                return <div style={{
                  position: "absolute",
                  top: 0,
                  left: 0,
                  width: '100%',
                  transform: `translateY(${item.index *itemSize}px)`,
                }} key={item.index}>item {item.index}</div>;
              })}
            </div>
          </div>
  );
}

可查看在线 demo: https://codesandbox.io/s/agit…

这就是一个最玲珑的虚构列表实例, 他次要分为 2 局部

  1. 须要有容器包裹, 并且应用 CSS 撑大高度, 理论渲染的 item 须要应用 transform 来显示到正确的地位
  2. 监听内部容器的滚动, 在滚动时, 动静切片原来的数据源, 同时替换须要显示的列表

来查看下他的性能:

根本没有阻塞, 偶然会有一点点失帧

这个 demo 并不是一个最终的状态, 他还有很多中央能够优化
比方缓存, 逻辑的提取, CSS 再度简化, 管制下滚动的触发频率, 滚动的方向管制等等, 有很多能够优化的点

其余库

  • react-virtualized 很多库举荐的虚构列表解决方案, 大而全
  • react-window react-virtualized 举荐的库, 更加轻量级代替计划。
  • react-virtual 虚构列表的 hooks 模式, 相似于我的 demo 中的逻辑封装

chrome 官网的反对

virtual-scroller

在 Chrome dev summit 2018 上,谷歌工程经理 Gray Norton 向咱们介绍 virtual-scroller,一个 Web 滚动组件,将来它可能会成为 Web 高层级 API(Layered
API)的一部分。它的指标是解决长列表的性能问题,打消离屏渲染。

然而, 开发了局部之后, 通过外部探讨, 还是先终止此 API, 转向 CSS 的开发
链接: https://github.com/WICG/virtu…
Chrome 对于 virtual-scroller 的介绍: https://chromestatus.com/feat…

content-visibility

这就是之后开发的新 CSS 属性

Chromium 85 开始有了 content-visibility 属性,这可能是对于晋升页面加载性能晋升最无效的 CSS 属性,content-visibility 让用户代理失常状况下跳过元素渲染工作(包含 layout 和 painting),除非须要的时候进行渲染工作。如果页面有大量离屏(off-screen)的内容,借助 content-visibility 属性能够跳过离屏内容的渲染,放慢用户首屏渲染工夫,能够做到缩小的页面可交互的等待时间

具体介绍: https://web.dev/content-visib…

应用形式是间接增加 CSS 属性

#content {content-visibility: auto;}

有点遗憾的是, 他的成果是加强渲染性能, 然而在大量数据初始化的时候, 依旧会卡顿, 没有虚构列表来的那么间接无效

然而在咱们减小首屏渲染工夫的时候能够思考利用起来

总结

在多数据下的性能优化, 有很多中解决方案

  • requestAnimationFrame
  • canvas
  • IntersectionObserver
  • startTransition
  • 虚构列表
  • content-visibility

总的来说虚构列表是最无效的, 同时也能够应用最简略 demo 级别来长期优化代码

退出移动版