关于前端:从一个-bug-中学习-canvas-最大内存限制和浏览器渲染原理

47次阅读

共计 7307 个字符,预计需要花费 19 分钟才能阅读完成。

注释

前几天一个共事让我帮忙解决一个 bug,这个 bug 困扰他好几天了。这是一个 App 中的 Hybrid 页面,它瀑布流中的图片总是划着划着有几张图片是白图(加载不进去),越往下划呈现的概率越大,而且这个问题只有 iOS 上才呈现,Android 是失常的。

首先我问他会不会是图片兼容性的问题,比方低版本的 Safari 是不反对 webp 格局的图片的。他说这个页面中的图片并不是 http-url 类型的图片,都是由 canvas 渲染进去的 base64 dataURL。那这就排除了图片格式兼容性的问题。

于是我向他更进一步理解业务场景是什么,为什么会用到 canvas。上面先给大家讲讲这个业务的应用场景:

首先我轻易搜寻一张图,比方『杨幂』

<img width=”375″ alt=”search” src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/847efe3cdef54df8b5eb23630fd253be~tplv-k3u1fbpfcp-watermark.image?”>

而后抉择一张图片点进去

<img width=”375″ alt=”blog_detail” src=”https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f1f461f4c5444f1b1891619c6e18f02~tplv-k3u1fbpfcp-watermark.image?”></img>

接着长按图片,会呈现一个弹框,弹框里会有图片丑化等相干的性能。

<img width=”375″ alt=”long_touch” src=”https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f8efcf9011c44a893b1f006adf77bbf~tplv-k3u1fbpfcp-watermark.image?”></img>

咱们点击『头像边框』按钮,会进入一个页面,页面里有一个这张图片被很多不同的装璜模板渲染的列表,每个模板里都有一些滤镜、小挂件、背景等等装璜。也就是把用户可能喜爱的搭配提前展现给用户看看,用户点进去就能够应用这个模板持续编辑图片了。

<img width=”375″ alt=”list” src=”https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f8648ad5371f46ef8f887e430fa34643~tplv-k3u1fbpfcp-watermark.image?”></img>

随着咱们往下滑动,能够发现有些本该渲染的图片是白图

<img width=”375″ alt=”little_bug” src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d35c1e9eeb9f45fdb727b4234fa6ac77~tplv-k3u1fbpfcp-watermark.image?”></img>

并且越往下白图的几率越大(到这里仅仅只有一张图片能失常渲染了)。

<img width=”375″ alt=”lots_of_bug” src=”https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eff2efabb97e4027ae0f25259d2bbea2~tplv-k3u1fbpfcp-watermark.image?”></img>

看完体现,接下来简略说一下渲染计划的选型。因为各种装璜模板实质上只是各种 json 配置,最终的渲染无非是轻前端 / 重前端,即,要么是前端把原图传给后端或客户端,让它们依据各种配置渲染完后返回给前端一个新图片;要么就是前端齐全本人渲染。

最终决定的计划是由前端渲染,一来能够尝试更多挑战;二来如果由客户端渲染,未来前端页面难以脱离成纯 web 页,控制权交给他人会让本人无奈面对更多变数;三来如果交给后端,会产生更多网络 io 的提早,升高用户体验。这种滤镜、多图片叠加的场景天然是用 canvas 比拟适合,而且因为之前咱们就在应用一个 canvas 库:Konva,所以这次还是应用了它。起初就呈现文章结尾提到的问题。

幸好咱们在开发环境下的挪动端页面接入了 devtool 工具,所以在手机上也是能看到报错信息的。报错如下:

<img width=”375″ alt=”error_info” src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef83e0bdfc004189a9f873c5f1cc863d~tplv-k3u1fbpfcp-zoom-1.image”></img>

可是那位共事示意他并没有用过 .scale 这个办法,那么会不会是引入的某个库做的呢,也因而没有 map 文件难以发现真正的代码地位。只能去我的项目 dist 搜寻。通过剖析我的项目 dist 代码,发现只有这么一处。

须要再追溯看看这个 _context 是什么。

红框圈进去的中央很显著就晓得,它就是 canvas.getContext('2d') 返回的 context。那么为什么 getContext('2d') 会返回 null 呢。

有一种可能是,先执行了getContext('3d'),又执行了getContext('2d'),显然这里不可能。

另一种可能是,不同的浏览器内核对 canvas 的解决策略不一样,它可能超出了 Safari 浏览器的限度,而 chrome 是没有这个限度,所以安卓手机是失常的。这一点是我在 stack overflow 看到有人这么说才确定的,这里贴个原文。

I’m not sure if this is relevant to the situation described here, but I had a similar situation which I was able to solve. Perhaps this helps somebody down the line. The gist of the problem was that browsers deal with HTML5 canvases quite differently. In particular, the amount of memory they are willing to allocate to canvases en total and to individual canvases (constraints on height, width, and area) seem to differ. I never bothered to grok the details, but here’s a stack question addressing some of the constraints

For me, I was generating many independent canvases and managing their 2d context separately. The problem is that I wasn’t being careful with garbage collection, and it took me a long time to notice it because I was testing for the most part in Chrome, in which everything worked fine.

Meanwhile the behavior in firefox was that my project became totally unresponsive without throwing meaningingful errors, and in Safari i could get my contexts fine until i ran out of total memory to allocate to canvases, and then getContext(‘2d’) would return null.

My first solution was to reduce the resolution of my canvases, but the better solution was to dispose of the ones I wasn’t currently using and generate them on the fly.

粗心是 chrome、Firefox、Safari 等浏览器对 canvas 的总内存占用限度、单个 canvas 的限度(如 width、height、像素密度)不尽相同。在大量应用 canvas 时没有留神即便回收,导致了他在 chrome 测试没问题的代码,Firefox 中齐全没有反馈,在 Safari 中报错。

有了类似状况的借鉴,大略晓得了问题所在,回到我的我的项目中看看能如何优化。为了尽快展现图片,瀑布流中每个图片尽管都须要一个 canvas 渲染,但图片理论应用的是 canvas 生成的 base64 dataURL,生成后 canvas 是不须要持续存在的,因而在拿到 url 后就应该 destroy 掉它。

于是在我这么做了之后,状况好了很多,有更多的图片失常了,但并没有齐全修复。起因很简略,因为刚开始的图片并发量太大了(顺带的就是 canvas 创立的并发量很大),只管 canvas 用完后会销毁掉,但大部分没来得及等到销毁,内存限度就到了。所以还须要限度每次分页的数量或者罗唆做成只在可视区域加载。我抉择的是后者计划,因为瀑布流这种场景,严格一点来讲数量能够看做有限多,当某些极其的场景,用户刷了几万个资讯,滚动起来是可能会卡顿的。虚构列表(virtual list)在这里既能够解决 canvas 太多的问题,也能够优化有限滚动性能。所以我须要虚构列表的加持,因为技术栈是 React,所以这里我抉择的是 react-virtualized 中的 Masonry。它只会渲染可视区域左近的元素,在有限滚动下也放弃了性能的优异。(对于滚动(渲染)性能为什么会和 dom 复杂度无关,请看『加餐』局部或者看我往期的精读文章《2. 浏览器原理系列 - 浏览器渲染流程详解》)

通过这些批改,并发的 canvas 创立曾经不多了(永远放弃在 10 个以内),用完的 canvas 也会被即时销毁,再也没有图片渲染失败的问题了。

加餐

后面咱们留下了两个纳闷,1. Safari 对 canvas 的内存限度到底是多少。2. 在浏览器渲染流程中,滚动到底干了什么。

canvas 内存限度

对于 1,咱们能够写一个循环创立 canvas 的代码,每个 canvas 宽高各 512px,也就是一个 512×512 像素的纯白色图片(32 位),它占用内存 1MB。通过一步步试探咱们就能够找出 chrome、Safari 等浏览器的限度是多少。上面看实际操作。

这里是试验代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
    />
    <title>canvas limit</title>
  </head>
  <body>
    <div>
      <span> 内存单位: MB</span>
      <input type="number" id="number" />
    </div>
    <div>
      <button id="create"> 创立 </button>
    </div>
    <script>
      // canvas 放进该全局变量,避免 GC
      let canvasQueue = [];

      // 创立 1MB canvas
      const create1MCanvas = () => {
        const size = 512;
        const canvas = document.createElement("canvas");
        canvas.width = size;
        canvas.height = size;
        const context = canvas.getContext("2d");
        context.fillRect(0, 0, size, size);
        return canvas;
      };

      // 创立 n x 1MB canvas
      const createNMCanvas = (n) => {for (let i = 0; i < n; i++) {canvasQueue.push(create1MCanvas());
        }
      };

      const input = document.querySelector("#number");
      const button = document.querySelector("#create");

      button.addEventListener("click", (event) => {event.preventDefault();
        const number = input.value;
        if (!Number.isNaN(Number(number))) {canvasQueue = [];
          createNMCanvas(Number(number));
          console.log(` 创立 ${number}MB canvas 胜利 `);
        }
      });
    </script>
  </body>
</html>

操作系统是 macOS 10.15.7

Chrome

咱们先在 chrome 试验一下每个 canvas 内存占用是否合乎预期的 1MB,chrome 版本:101.0.4951.54(正式版本)(x86_64)

先记录一下初始 GPU 过程 内存占用(375MB)

接着咱们试着创立 1000 个 1MB canvas,也就是占用 1GB 内存

再来看看 GPU 过程内存占用(1.3GB)

的确增长了 1GB 左右,因而证实咱们的测试程序是精确的。上面咱们开始测试 chrome 有没有 Safari 那样的内存下限。

咱们间接创立 10 万个 canvas,占用内存 10GB(PC 总内存是 16GB)

后果是胜利的,因而能够推断出 chrome 简直没有对 canvas 做内存限度,只有设施内存够大就能吃得下。

Safari

敞开 chrome 页面开释内存后,接着咱们同样在 Safari 关上这个页面,Safari 版本 15.5 (15613.2.7.1.9, 15613)。

通过一步步减少内存占用,试出了 Safari 的最大限度(4096MB)

至此,对于两大支流浏览器对 canvas 限度的解惑完结了。

浏览器渲染流水线与滚动

首先简略讲一下 chrome 中渲染流水线的流程,一个 html 是怎么被解决成一个页面的。html 中 dom 局部生成 dom tree,css 局部生成 stylesheet,dom tree 在解析完后会期待 stylesheet 构建完再渲染。stylesheet 依据默认款式、款式继承、css 选择器规定、款式优先级等规定,找到对应的 dom 节点赋予它款式,造成 dom 构造 + 款式的 render tree。其中有些节点是不可见的(不是 opacity: 0,而是诸如 display: none 这样的),它们不会影响其余节点的地位,在渲染时不须要思考,所以过滤掉这些节点之后生成 layout tree,layout tree 会依据节点之间的相互影响生成它们的地位信息(reflow 回流 / 重排)。

你认为到了 layout tree 这一步终于能够渲染了吗,还远没有。layout tree 会依据某些 css 属性分层,比方 position: absolute; position: fixed 等等。如果它们产生更新,不须要连带其余节点 reflow,所以分层有利于独自解决。而后每个图层会生成各自的绘制指令列表(repaint 重绘),很底层的命令,形容了每一个点每一条线如何绘制。

你认为生成了绘制命令终于能够渲染了吗,还远没有。它们会被交给合成层,顾名思义,它是负责将那些图层合并的。它并不会全量的解决整个页面,而是优先解决可见视口左近的图块,如果页面过于简单,它还会先给出低分辨率的位图。图块的绘制命令会通过光栅化线程池交给 gpu 绘制。合成层拿到 gpu 绘制进去的位图后,将它们合成为一张位图,这就是以后页面。你感觉终于渲染完了吗,并没有。它会将位图交给浏览器过程里的 biz 组件,biz 组件会交给显示器的后缓冲区,当显示器须要显示下一帧之前,前后缓冲区替换,屏幕上终于展现出新渲染的页面帧。(这里补个小常识,requestAnimationFrame 的背地原理就是显示器发送了 sync 信号,渲染过程将 requestAnimationFrame 回调放进音讯队列,从而实现了 js 未阻塞的状况下 requestAnimationFrame 能够随帧调用)

能够发现浏览器想渲染一帧页面要通过如此多的步骤,是不是真为它的性能捏一把汗,这也是 HTML 不便开发带来的代价。咱们还能够看出整个流程中最低廉的步骤就是 reflow 和 repaint 了,至于合成层那边次要是和 GPU 打交道,不会占用渲染线程(也就是执行 js 的线程),并且 GPU 原本就非常善于解决图片,所以合成层的工作很快。

因而,性能优化的外围思路其实就是尽可能减少 reflow、repaint 的工作,尽可能多利用合成层的工作。比方 css 硬件加速,包含 transform3D、opacity、willchange 等。拿 transform3D 来说,其实它只是图层的位移、转换,并不影响其余图层,所以不会通过 reflow 和 repaint,间接在合成层解决,GPU 解决这种变换十分快。因而硬件加速技巧能够极大的优化 css 性能。

上面谈一下滚动操作带来的影响在渲染流水线中的处于什么地位。首先滚动可能会产生滚动条,它的忽然呈现影响了其余元素的布局地位,会触发 reflow 以及前面的所有流程。滚动过程中,后面说过合成层初始优先解决可见视口左近的图块,其余局部其实还没有解决,元素构造太简单时滚动过快可能让合成层来不及解决,从而呈现白屏区域。还有比方 position: fixed 的元素,会跟着滚动走,那么它也会在滚动中 repaint 的。

写在最初

为什么要学习原理(不论是编程畛域还是其余畛域),因为世间无穷的景象都能够用无限的原理解释,如果只停留于景象就会陷入经验主义。

正文完
 0