在一个前端项目中,我遇到过 Plotly.js WebGL 实例数量限制 的问题。Plotly 每创建一个 WebGL 图表都会占用一个上下文,而浏览器通常有 8\~16 个 WebGL 上下文上限。一旦超出,就会报错无法渲染。这个问题在需要并行显示多个 Plotly WebGL 图时尤其明显。

第一步:引入 virtual-webgl.js 解决 Plotly 问题

virtual-webgl 提供了一个虚拟层,允许多个 WebGL 图表共享一个虚拟的上下文。于是我在项目中全局引入:

import '@/assets/virtual-webgl.js';

这确实解决了 Plotly WebGL 实例过多导致的报错,多个 Plotly 图表可以并行渲染,不再受限于浏览器原生的 WebGL 上下文数量。

第二步:发现 Highcharts 与 virtual-webgl 不兼容

但是问题来了:Highcharts 在 5000+ 数据点时会自动启用 Boost 模式,尝试用 WebGL 来提升性能。但因为此时的 getContext('webgl') 已被 virtual-webgl 接管,Highcharts 得到的是虚拟上下文,结果直接画不出来。

于是我面临一个两难:

  • Plotly 必须依赖 virtual-webgl。
  • Highcharts 必须依赖原生 WebGL 或 Canvas。

第三步:尝试方案探索

一开始我尝试了几种思路:

  1. 按需启用 virtual-webgl:在 Plotly 渲染时加载,在 Highcharts 渲染时卸载。但问题是两个库可能并行渲染,无法用简单的“先后切换”解决。
  2. 禁用 Highcharts Boost:强制 Highcharts 使用 Canvas 渲染,虽然能画出来,但 1w+ 数据点性能明显下降。

最后想到的方向是:做一个 getContext 的“多路复用器”(MUX),针对不同 canvas 返回不同的上下文。

第四步:实现多路复用器(MUX)

思路:

  1. 在 virtual-webgl 改写 getContext 之前,先缓存原生方法:
// native-gl.ts
export const NATIVE = {
  getContext: HTMLCanvasElement.prototype.getContext,
  WebGLRenderingContext: (window as any).WebGLRenderingContext,
  WebGL2RenderingContext: (window as any).WebGL2RenderingContext,
};
  1. 在引入 virtual-webgl 之后,再写一个 mux-install.ts,重新挂载路由:
import { NATIVE } from './native-gl';

const VIRTUAL_GETCONTEXT = HTMLCanvasElement.prototype.getContext;

function isGL(type: any) {
  return type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl';
}
function isPlotlyCanvas(el: HTMLCanvasElement) {
  return el.classList.contains('gl-canvas') || !!el.closest('.js-plotly-plot');
}
function isHighchartsCanvas(el: HTMLCanvasElement) {
  return !!(el.closest?.('.highcharts-container') || (el as any).dataset?.highchartsChart);
}

HTMLCanvasElement.prototype.getContext = function (type: any, attrs?: any) {
  if (!isGL(type)) {
    return NATIVE.getContext.call(this, type, attrs);
  }
  const el = this as HTMLCanvasElement;
  if (isPlotlyCanvas(el) && !isHighchartsCanvas(el)) {
    return (VIRTUAL_GETCONTEXT as any).call(el, type, attrs);
  }
  return NATIVE.getContext.call(el, type, attrs);
};
  1. 在入口 app.tsx 中严格按顺序引入:
import '@/assets/native-gl.ts';   // 缓存原生
import '@/assets/virtual-webgl.js'; // 注入虚拟
import '@/assets/mux-install.ts';   // 安装路由

第五步:验证结果

  • Plotly:依然通过 virtual-webgl 渲染,避免了实例数量上限问题。
  • Highcharts:识别为 Highcharts 的 canvas 时,强制走原生 WebGL,上千点数据也能正常渲染。
  • 并行渲染场景:Plotly 和 Highcharts 可以同时画图,互不干扰。

经验总结

  1. virtual-webgl 不是银弹,它改变了浏览器的全局 API,必然会影响到依赖 WebGL 的其他库。
  2. 解决冲突的关键在于“路由”:把 getContext 按画布归属分流,让不同库拿到合适的上下文。
  3. 要注意加载顺序:必须先缓存原生,再注入 virtual-webgl,最后安装路由,否则容易踩坑。
  4. 打标签有助于识别:在封装组件时,可以给 Plotly/Highcharts 容器加上自定义属性,进一步降低误判风险。

通过这套改造,项目里同时用 Plotly.js(共享虚拟 WebGL)和 Highcharts(走原生 WebGL)变得完全可行。这个实践让我意识到:在前端工程里引入低层图形库时,一定要考虑全局副作用,必要时做隔离和路由,才能让多个库和平共处。


A Student on the way to full stack of Web3.