使用OffscreenCanvas加速图表

图表渲染会严重影响浏览器。尤其是在复杂应用程序的界面中输出代表图表的大量元素时。您可以尝试使用界面来改善这种情况,该界面浏览器OffscreenCanvas支持程度正在逐渐提高。它允许使用网络工作人员将形成可见图像中显示的图像的任务移交给它<canvas> 这篇文章(我们今天将要翻译的译本)致力于使用接口在这里,我们将讨论为什么需要此接口,使用该接口的实际期望以及使用该接口可能遇到的困难。



OffscreenCanvas

为什么要注意OffscreenCanvas?


绘制图时涉及的代码本身可能具有足够高的计算复杂度。在许多地方,您可以找到有关如何显示平滑动画以及如何确保用户与页面进行方便交互的详细故事,一帧的渲染必须在大约10毫秒内适应,这样您才能达到每秒60帧。如果输出一帧所需的计算花费10毫秒以上,这将导致页面的明显“减速”。

但是,在显示图时,以下情况会加剧这种情况:

  • — - (SVG, canvas, WebGL) , , .
  • , , , , 10 . ( — ) .

这些问题是称为分而治之的经典优化方法的理想选择。在这种情况下,我们正在讨论跨多个线程的计算负载分配。但是,在界面出现之前,OffscreenCanvas所有渲染代码都需要在主线程中执行。此代码可以使用所需API的唯一方法。

从技术角度来看,“繁重的”计算可以更早地输出到Web Worker线程。但是,由于必须从主流进行负责渲染的调用,因此需要在图表输出代码中的工作流和主流之间使用复杂的消息交换方案。而且,这种方法通常只会带来轻微的性能提升。

规格OffscreenCanvas为我们提供了一种<canvas>在Web Worker中将表面控制转移到显示元素图形的机制Chrome和Edge浏览器当前支持该规范(将Edge迁移到Chromium之后)。预计OffscreenCanvas将在六个月内出现对Firefox的支持如果我们谈论的是Safari,则尚不清楚是否计划在此浏览器中支持该技术。

使用OffscreenCanvas输出图表



显示100,000个彩色点的图表示例

为了更好地评估潜在的优缺点OffscreenCanvas,让我们看一下上图中显示的“重”图的输出示例。

const offscreenCanvas = canvasContainer
  .querySelector('canvas')
  .transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

首先,我们使用new方法查询OffscreenCanvas元素。然后,我们调用方法,执行此操作以将包含指向的链接的消息发送给工作程序。在这里,作为第二个参数,您需要使用非常重要。这使您可以使该对象工作者成为所有者,这将使他可以单独进行管理canvastransferControlToOffscreen()worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas])offscreenCanvas[offscreenCanvas]OffscreenCanvas

canvasContainer.addEventListener('measure', ({ detail }) => {
  const { width, height } = detail;
  worker.postMessage({ width, height });
});
canvasContainer.requestRedraw();

考虑到大小OffscreenCanvas最初是从属性widthheight元素继承的canvas,因此更改元素的尺寸时使这些值保持最新是我们的责任canvas。在这里,我们使用measure来自的事件d3fc-canvas,这将使我们有机会与requestAnimationFrame工作人员交流有关元素的新尺寸的信息canvas

为了简化示例,我们将使用d3fc库中的组件。这是一组辅助组件,它们可以集合d3组件或补充其功能。您OffscreenCanvas可以使用不带d3fc组件的产品。可以使用专有的标准JavaScript功能完成所有讨论。

现在转到文件中的代码worker.js。在此示例中,为了实际渲染性能,我们将使用WebGL。

addEventListener('message', ({ data: { offscreenCanvas, width, height } }) => {
    if (offscreenCanvas != null) {
        const gl = offscreenCanvas.getContext('webgl');
        series.context(gl);
        series(data);
    }

    if (width != null && height != null) {
        const gl = series.context();
        gl.canvas.width = width;
        gl.canvas.height = height;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    }
});

当我们收到包含property的消息时canvas,我们假定该消息来自主线程。接下来,我们从元素获取canvas上下文webgl并将其传递给component series。然后,我们调用该组件series并向其传递data要使用它呈现的数据()(下面将讨论相应变量的来源)。

另外,我们检查消息中的属性widthheight,并使用它们来设置WebGL 的尺寸offscreenCanvas查看区域。链接offscreenCanvas不直接使用。事实是该消息包含一个offscreenCanvas或多个属性widthheight

其余的工作程序代码负责设置我们要输出的内容的呈现。这里没有什么特别的,因此,如果您熟悉所有这些内容,则可以立即进入下一节讨论性能的部分。

const randomNormal = d3.randomNormal(0, 1);
const randomLogNormal = d3.randomLogNormal();

const data = Array.from({ length: 1e5 }, () => ({
    x: randomNormal(),
    y: randomNormal(),
    size: randomLogNormal() * 10
}));

const xScale = d3.scaleLinear().domain([-5, 5]);

const yScale = d3.scaleLinear().domain([-5, 5]);

首先,我们创建一个数据集,其中包含围绕原点x / y随机分布的点的坐标,并调整缩放比例。我们不使用该方法设置x和y值的范围range,因为WebGL系列canvas (-1 -> +1在设备的标准化坐标中以元素的完整尺寸显示)。

const series = fc
    .seriesWebglPoint()
    .xScale(xScale)
    .yScale(yScale)
    .crossValue(d => d.x)
    .mainValue(d => d.y)
    .size(d => d.size)
    .equals((previousData, data) => previousData.length > 0);

接下来,我们使用xScale设置一系列点yScale然后,我们配置允许您读取数据的适当访问方式。

此外,我们定义了自己的相等性检查功能,该功能旨在确保组件data在每次渲染时都不会传输GPU。我们必须明确表示这一点,因为即使我们知道我们不会修改此数据,该组件也无法在不执行资源密集型“脏”检查的情况下知道它。

const colorScale = d3.scaleOrdinal(d3.schemeAccent);

const webglColor = color => {
    const { r, g, b, opacity } = d3.color(color).rgb();
    return [r / 255, g / 255, b / 255, opacity];
};

const fillColor = fc
    .webglFillColor()
    .value((d, i) => webglColor(colorScale(i)))
    .data(data);

series.decorate(program => {
    fillColor(program);
});

此代码使您可以为图表着色。我们使用数据集中的点的索引从中选择合适的颜色colorScale,然后将其转换为所需的格式,并用它来装饰输出点。

function render() {
    const ease = 5 * (0.51 + 0.49 * Math.sin(Date.now() / 1e3));
    xScale.domain([-ease, ease]);
    yScale.domain([-ease, ease]);
    series(data);
    requestAnimationFrame(render);
}

现在,点已着色,剩下的就是为图表制作动画了。因此,我们将需要一个常量方法调用render()。此外,这还会给系统造成一定的负担。我们使用requestAnimationFrame修改属性xScale.domainyScale.domain每一帧模拟图表的增加和减少。在此应用和计算与时间有关的值,以便图表的比例平滑变化。另外,我们修改了消息头,以便调用一个方法以开始呈现周期render(),从而不必直接调用series(data)

importScripts(
    './node_modules/d3-array/dist/d3-array.js',
    './node_modules/d3-collection/dist/d3-collection.js',
    './node_modules/d3-color/dist/d3-color.js',
    './node_modules/d3-interpolate/dist/d3-interpolate.js',
    './node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
    './node_modules/d3-random/dist/d3-random.js',
    './node_modules/d3-scale/dist/d3-scale.js',
    './node_modules/d3-shape/dist/d3-shape.js',
    './node_modules/d3fc-extent/build/d3fc-extent.js',
    './node_modules/d3fc-random-data/build/d3fc-random-data.js',
    './node_modules/d3fc-rebind/build/d3fc-rebind.js',
    './node_modules/d3fc-series/build/d3fc-series.js',
    './node_modules/d3fc-webgl/build/d3fc-webgl.js'
);

为了使该示例正常工作,仅需将必需的库导入工作器中。我们为此使用importScripts,并且为了不使用构建项目的工具,我们使用手动编译的依赖项列表。不幸的是,我们不能仅下载完整的d3 / d3fc构建,因为它们是DOM依赖的,并且无法满足工作程序中的需求。

→此示例的完整代码可以在GitHub找到

屏幕外画布和性能


我们项目中的动画作品得益于requestAnimationFrame工人的使用。即使主线程忙于其他事情,这也允许工作人员呈现页面。如果您查看上一部分中描述项目页面并单击按钮Stop main thread,您会注意到以下事实:当显示消息框时,时间戳信息的更新将停止。主线程被阻止,但图表动画不会停止。


项目窗口

我们可以通过从主流接收消息来组织渲染。例如,当用户与图表交互或通过网络接收新数据时,可以发送此类事件。但是使用这种方法,如果主线程忙于处理某些事情,而在工作进程中未收到任何消息,则渲染将停止。

在什么都没发生的情况下连续渲染图表是用嗡嗡作响的风扇和低电量使用户烦恼的好方法。结果是,在现实世界中,如何在主线程和工作线程之间划分责任的决定取决于工作人员在未从主线程接收到有关情况变化的消息时是否可以提供有用的信息。

如果我们谈论线程之间的消息传输,则应注意,此处我们采用了一种非常简单的方法。老实说,我们需要在线程之间传输很少的消息,所以我宁愿称我们的方法是实用主义而不是懒惰的结果。但是,如果我们谈论的是实际应用,则可以注意到,如果用户与图表的交互以及可视化数据的更新将取决于它们,则流之间的消息传输方案将变得更加复杂。

您可以使用标准MessageChannel接口在主线程和工作线程之间创建单独的消息通道。这些通道中的每一个都可用于特定类别的消息,这使得处理消息更加容易。作为标准机制的替代,像Comlink这样的第三方库也可以起作用。该库使用代理对象将高级细节隐藏在高级接口后面

我们的示例的另一个有趣特征(回顾起来很明显)是这样一个事实,即它考虑到GPU资源(与其他系统资源一样)并非无尽的事实。将渲染转换为工作程序可以使主线程解决其他问题。但是无论如何,浏览器都需要使用GPU来渲染DOM以及通过多种方式生成的内容OffscreenCanvas

如果工作人员消耗了所有GPU资源,则无论在何处进行渲染,主应用程序窗口都将遇到性能问题。注意示例中时间戳更新速度随着显示点数的增加而降低。如果您有功能强大的图形卡,则可能必须增加查询字符串中传输到页面的点数。为此,您可以使用超过最大值100000的值,该值是通过示例页面上的链接之一指定的。

应当指出,这里我们没有探索上下文属性的秘密世界这样的研究可以帮助我们提高解决方案的生产率。但是,由于对这些属性的支持水平较低,所以我没有这样做。

如果我们讨论使用渲染OffscreenCanvas,则该属性在这里看起来最有趣desynchronized在受支持的地方,并且考虑到一些限制,它使您可以摆脱事件循环和在工作程序中执行的呈现循环之间的同步。这样可以最小化更新图像的延迟。有关此的详细信息,请参见此处

摘要


该界面OffscreenCanvas为开发人员提供了改善图表呈现性能的机会,但是使用它需要周到的方法。使用它,您需要考虑以下几点:

  • (, , ) - .

    • , . , , , postMessage.
  • , (, SVG/HTML- <canvas>), , , .

    • , , , , , , , , .
  • , GPU, OffscreenCanvas .

    • OffscreenCanvas, GPU-, , . , , .

这是示例代码,是其工作版本。

亲爱的读者们!您是否使用OffscreenCanvas来加快网页上图形的显示?


All Articles