Rendimiento

Un pipeline Canvas2D de dos canvas solo vuelve a pintar lo que ha cambiado, y cada paso por fotograma depende de lo que hay en pantalla, no de cuánto historial está cargado. Las cifras de abajo proceden de pnpm bench (un solo núcleo) y del perfilado del widget en vivo; el Feature Lab mide los cambios reales a medida que haces clic.

● DemosMapa de calor de liquidezDatos de mercado simulados, generados en tu navegador

Fotogramas: estables de 500 a 100,000 barras

  • Dos canvas: un canvas de escena (cuadrícula, series, indicadores, objetos del gráfico, ejes) y un canvas superior ligero para la cruz, la leyenda y demás elementos ligados al puntero. Al pasar el cursor solo se vuelve a pintar el canvas superior (~0.2 ms), y el navegador compone dos superficies, no cuatro.
  • Renderizado del rango visible: todos los renderizadores, la escala automática y el rango de precios de los indicadores recorren solo las barras a la vista.
  • Sin basura por fotograma: las instantáneas de la vista se guardan en caché entre cambios; los formateadores de números y fechas se reutilizan en lugar de recrearse para cada etiqueta.
  • Eje de precio de tamaño automático: ajustar el eje a su etiqueta más larga cuesta ~0.05 ms y solo se recalcula el diseño cuando el ancho cambia de verdad.

Ticks en vivo: indicadores incrementales

Un tick solo mueve la barra en formación, así que un indicador con su propio update() recalcula únicamente esa barra: ATR, Bollinger Bands, Chande Kroll Stop, DEMA, EMA, Envelope, KAMA, MACD, MA Cross, McGinley Dynamic, OBV, RSI, SMA, SMMA, Stochastic, Ulcer Index, Volume Oscillator, VWMA, WMA. Otros 31 se construyen sobre PointwiseIndicator y continúan desde la última barra del mismo modo, y un indicador que lee la línea de otro también continúa. Cuando las barras que recuerda un indicador ya no coinciden, recalcula todo. BB + EMA + RSI + MACD:

HistorialRecálculo completoupdate() incremental
20,000 barras~5 ms~0.0005 ms
100,000 barras~27 ms~0.001 ms

Los indicadores personalizados también pueden aprovecharlo; consulta Plugins → actualizaciones rápidas en vivo.

Cambio de símbolo o de temporalidad

  • Cargas completas baratas: los valores de los indicadores viven en un IndicatorValueMap (basado en arrays, ~3× más barato de construir que un Map indexado por marca de tiempo), y setData reutiliza las barras que ya son válidas en lugar de copiarlas.
  • Indicador de carga solo cuando es lento: el gráfico anterior sigue visible; solo aparece un velo si un cambio dura más de 200 ms, así que un cambio de red normal de ~130 ms nunca parpadea.
  • Sin datos obsoletos: las solicitudes de historial superadas se descartan y los sockets antiguos se desconectan, así que el último clic siempre gana.
  • Remuestreo local: con datos estáticos se cambia de temporalidad sin volver a descargar; los remuestreos lentos pintan el velo antes de que el hilo principal se ocupe.

Submuestreo LTTB

Los gráficos de línea y de área submuestrean el rango visible a ~2 puntos por píxel con Largest-Triangle-Three-Buckets cuando hay muchas más barras que píxeles: la línea se ve idéntica mientras se dibujan decenas de veces menos puntos. Con el zoom normal no hace nada. El algoritmo se exporta para que lo uses tú también:

import { lttbDownsample } from '@tradecanvas/chart'

// indices preserving the shape of a 100k series, reduced to 1600 points
const idx = lttbDownsample(series.length, 1600, (i) => series[i].close)
Puntos visibles → 1600Tiempo / fotogramaRendimiento
10,000~0.025 ms39,600 / s
100,000~0.32 ms3,100 / s
1,000,000~2.6 ms380 / s

Indicadores con zoom alejado

Cuando cada barra mide menos de un píxel, las líneas, bandas e histogramas de los indicadores se dibujan con un tramo por columna de píxeles: del punto más bajo al más alto de la columna, unido a la columna anterior y tan ancho como la línea. Se ve casi igual que un trazo por miles de puntos con una fracción del trabajo de rasterizado. Con el zoom alejado sobre 200.000 barras con Bandas de Bollinger, EMA, RSI y MACD, un fotograma pasó de unos 54 ms a unos 21 ms en una GPU integrada. node scripts/bench-render.mjs mide estas cifras en tu propio equipo.

Renderizador WebGL (versión preliminar)

renderer: 'webgl' dibuja el área del gráfico y los paneles de indicadores con WebGL 2, en un canvas bajo la escena 2D: la cuadrícula, las sesiones, las velas y el volumen directamente, y el dibujo en Canvas 2D de los indicadores, las líneas de comparación y la mayoría de los tipos de gráfico se graba como trazos, rellenos y rectángulos de la GPU, con los bordes suavizados como en Canvas 2D. El mapa de calor de profundidad y los perfiles de volumen y de mercado se graban igual, bajo las barras; un perfil de mercado que muestra su recuadro de estadísticas o sus letras sigue en Canvas 2D. El texto, los dibujos, las órdenes, los ejes y la cruceta siguen en Canvas 2D, igual que todo lo que la GPU no dibujaría igual (se deja a Canvas 2D en orden, así que el apilado no cambia); los plugins de indicadores propios funcionan sin cambios. Las velas caen en los mismos píxeles del dispositivo que con Canvas 2D; las líneas y los rellenos solo difieren en algunos píxeles de borde suavizados. El código WebGL es un chunk propio (unos 17 KB con gzip) que se carga la primera vez que se usa; donde falta WebGL 2, o si se pierde el contexto, el gráfico sigue dibujando con Canvas 2D. Si el contexto vuelve, WebGL también. Como mucho 8 gráficos de una página dibujan con WebGL a la vez (setMaxWebGLCharts), porque Chrome y Safari solo mantienen unos 16 contextos WebGL por página y descartan el más antiguo al pasarse; un gráfico por encima del límite dibuja con Canvas 2D hasta que se libere uno. Comprobado en Chrome, Firefox y WebKit.

const chart = new Chart(el, { renderer: 'webgl' })

chart.on('rendererChange', (e) => console.log(e.payload))
// { renderer: 'webgl', reason?: 'contextRestored' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' | 'limit' }

await chart.setRenderer('canvas')   // resolves to what draws now

setMaxWebGLCharts(4)   // from '@tradecanvas/chart': at most 4 charts on the page draw with WebGL (8 by default)

Tiempo por fotograma al desplazar, en una GPU integrada (Intel UHD; 16.7 ms son 60 fps):

GráficoDensidad de píxelesCanvas 2DWebGL
1600×900, 500 velas + 4 indicadores227.4 ms19.6 ms
1600×900, zoom alejado sobre 200,000 barras + 4 indicadores234.5 ms20.2 ms
1600×900, zoom alejado sobre 1,000,000 barras + 4 indicadores234.5 ms16.7 ms
1600×900, mapa de calor de profundidad, 240 instantáneas × 80 niveles225.5 ms16.8 ms
Seis gráficos, cada uno con 500 velas y dos indicadores223.5 ms17.2 ms
2560×1400, 2,000 velas + 4 indicadores141.6 ms17.7 ms
2560×1400, 2,000 velas + 4 indicadores1.570.8 ms17.6 ms
2560×1400, 2,000 velas + 4 indicadores2114.5 ms29.1 ms
2560×1400, 2,000 velas233.1 ms20.9 ms

La mayoría de los fotogramas WebGL de arriba se quedan en 16.7 ms; las medias incluyen algunos más largos. Con densidad 2 en un gráfico de 2560×1400, solo la composición de las capas a tamaño completo ya le cuesta al navegador unos 23 ms en esta GPU. node scripts/bench-render.mjs --renderer=webgl mide estas cifras en tu equipo.

Una barra nunca es más estrecha que un cuarto de píxel CSS, así que «zoom alejado sobre 1,000,000 barras» muestra unas cuatro barras por píxel (unas 6,400 en un gráfico de 1600 px); la escena recorre el resto del millón al desplazarse.

Móviles y apps

Un móvil tiene una pantalla 3x, una CPU más lenta y una batería. El gráfico dibuja como mucho a una densidad de píxeles de 2 (maxPixelRatio): un 56% menos de píxeles que a 3x, con líneas apenas más suaves. Deja de dibujar mientras está fuera de la pantalla o la página está oculta (pauseWhenHidden), y una app que lo integra en un WebView lo pausa con suspend(). Con un feed en vivo que manda ticks más rápido que los fotogramas, los indicadores se recalculan una vez por fotograma, antes de que algo los lea. Cuando vuelven la página o la red, un flujo callado demasiado tiempo se reconecta enseguida en vez de esperar al latido; un gráfico destruido devuelve enseguida la memoria de sus lienzos.

new Chart(host, {
  maxPixelRatio: 2,       // the default; Infinity draws at the screen's own
  pauseWhenHidden: true,  // the default: no frames off screen or on a hidden page
})
chart.suspend()   // your app went to the background (a WebView): stop drawing
chart.resume()    // back: one frame draws what changed

Medido en una pantalla de móvil (390 × 844 a 3x, la CPU 4 veces más lenta, una GPU integrada), un gráfico con cuatro indicadores:

AntesAhora
Dibujar la escena, 300 barras34 ms25 ms
En vivo, 2 ticks por fotograma18.5 ms11–13 ms
Memoria de los lienzos17.1 MB7.6 MB

El widget en una pantalla táctil: campos a 16 px para que la página no haga zoom al enfocarlos, paneles por encima del teclado en pantalla, alturas en unidades de viewport dinámicas. Para medir en su equipo: node scripts/bench-render.mjs --mobile.

El hilo principal

El gráfico calcula los indicadores en el hilo principal. IndicatorWorkerHost de @tradecanvas/core puede ejecutar calculate() en un worker que tú proporciones, con una API basada en Promise, un tiempo de espera por solicitud y una alternativa síncrona para SSR y tests, pero el gráfico todavía no pasa sus indicadores por él y los paquetes no incluyen un script de worker.