Imagen de Wikipedia R‑tree

Biblioteca de visualización de grafos: cómo resolvimos el dilema Canvas vs. HTML
Biblioteca de visualización de grafos: cómo resolvimos el dilema Canvas vs. HTML
¡Hola! Me llamo Andréi y soy desarrollador de interfaces en el equipo de User Experience de los servicios de infraestructura de Yandex. Desarrollamos Gravity UI — un sistema de diseño open source y una biblioteca de componentes React que utilizan decenas de productos dentro de la empresa y fuera de ella. Hoy contaré cómo nos encontramos con la tarea de visualizar grafos complejos, por qué las soluciones existentes no nos convencieron y cómo finalmente apareció @gravity‑ui/graph — una biblioteca que decidimos abrir a la comunidad.
Esta historia empezó con un problema práctico: necesitábamos renderizar grafos de 10.000+ elementos con componentes interactivos. En Yandex hay muchos proyectos en los que los usuarios crean pipelines complejos de procesamiento de datos — desde procesos ETL sencillos hasta machine learning. Cuando esos pipelines se generan de forma programática, el número de bloques puede llegar a decenas de miles.
Las soluciones existentes no nos satisfacían:
- Bibliotecas HTML/SVG se ven bien y son cómodas para desarrollar, pero empiezan a ir lentas ya con cientos de elementos.
- Soluciones Canvas ofrecen rendimiento, pero requieren una enorme cantidad de código para crear elementos de UI complejos.
Dibujar un botón con esquinas redondeadas y un degradado en Canvas no es difícil. Pero aparecen problemas cuando hay que crear controles complejos propios o maquetación — habrá que escribir decenas de líneas de comandos de dibujo de bajo nivel. Cada elemento de interfaz hay que programarlo desde cero — desde el manejo de clics hasta animaciones. Y nosotros necesitábamos componentes de UI completos: botones, selects, campos de entrada, drag‑and‑drop.
Decidimos no elegir entre Canvas y HTML, sino usar lo mejor de ambas tecnologías. La idea era simple: cambiar automáticamente entre modos según lo cerca que el usuario esté viendo el grafo.

Pruébalo tú mismo
De dónde salió la tarea
Nirvana y sus grafos
En Yandex tenemos el servicio Nirvana para crear y ejecutar grafos de procesamiento de datos (sobre él escribimos ya en 2018). Es un servicio grande, popular y existe desde hace mucho.
Parte de los usuarios crea grafos manualmente — con el ratón, añaden bloques, los conectan. Con esos grafos no hay problema: hay pocos bloques y todo funciona excelente. Pero hay proyectos que crean grafos de forma programática. Y ahí empiezan las dificultades: pueden meter en un solo grafo hasta 10.000 operaciones. Y queda algo así:

Y también así:





Grafos así, una combinación habitual de HTML + SVG simplemente no los soporta. El navegador empieza a ir lento, hay fugas de memoria y el usuario sufre. Intentamos resolverlo de frente: optimizar el renderizado HTML, pero tarde o temprano chocábamos con límites físicos — el DOM no está pensado para miles de elementos interactivos flotantes visibles al mismo tiempo.
Necesitábamos otra solución, y en el navegador solo nos quedaba Canvas. Solo él puede garantizar el rendimiento necesario.
La primera idea fue encontrar una solución ya hecha. Era 2017–2018, y revisamos bibliotecas populares para Canvas o renderizado de grafos, pero todas tropezaban con el mismo problema: o usas Canvas con elementos primitivos, o usas HTML/SVG sacrificando rendimiento.
¿Y si no elegimos?
Level of Details: inspiración de GameDev
En GameDev y cartografía hay un concepto genial — Level of Details (LOD). Esta técnica nació de una necesidad: ¿cómo mostrar un mundo enorme sin matar el rendimiento?
La idea es simple: un mismo objeto puede tener varios niveles de detalle según lo cerca que se mire. En los juegos se nota especialmente:
- A lo lejos se ven montañas — polígonos simples con textura básica.
- Al acercarte, aparecen detalles: hierba, piedras, sombras.
- Aún más cerca, se ven hojas individuales en los árboles.
Nadie renderiza millones de polígonos de hierba cuando el jugador está en la cima de una montaña mirando a lo lejos.
En los mapas el principio es el mismo — cada nivel de zoom tiene su propio conjunto de datos y su propia granularidad:
- Zoom de continente — solo se ven países.
- Te acercas a una ciudad — aparecen calles y barrios.
- Más cerca — números de portal, cafeterías, paradas de autobús.
Entendimos: el usuario no necesita botones interactivos en una vista general de un grafo de 10.000 bloques — no los verá ni podrá trabajar con ellos.
Además, intentar renderizar 10.000 elementos HTML a la vez congelará el navegador. Pero cuando hace zoom en un área concreta, la cantidad de bloques visibles cae de golpe — de 10.000 a, digamos, 50. Ahí es donde se liberan recursos para componentes HTML con interactividad rica.
Tres niveles de nuestro esquema Level of Details
Minimalistic (zoom 0,1–0,3) — Canvas con primitivas simples
En este modo el usuario ve la arquitectura general del sistema: dónde están los principales grupos de bloques y cómo se conectan. Cada bloque es un rectángulo simple con codificación de color básica. Sin textos, botones ni iconos detallados. A cambio, se pueden renderizar miles de elementos de forma cómoda. En este nivel el usuario elige el área para un estudio detallado.

Schematic (zoom 0,3–0,7) — Canvas con detalles
Aparecen los nombres de los bloques, iconos de estado y anclajes para conexiones. El texto se renderiza con la API de Canvas — es rápido, pero las posibilidades de estilo son limitadas. Las conexiones entre bloques se vuelven más informativas: se puede mostrar la dirección del flujo de datos y el estado de la conexión. Es un modo de transición donde el rendimiento de Canvas se combina con información básica.

Detailed (zoom 0,7+) — HTML con interactividad completa
Aquí los bloques se convierten en componentes de interfaz completos: con botones de control, campos de parámetros, barras de progreso, selects. Se puede usar cualquier capacidad de HTML/CSS y conectar bibliotecas de UI. En este modo, normalmente caben en el viewport no más de 20–50 bloques, lo cual es cómodo para trabajo detallado.

¿Y si usamos FPS para elegir el nivel de detalle?
Probamos enfoques para seleccionar el nivel de detalle basándonos en FPS. Pero resultó que ese enfoque crea inestabilidad: si mejora el rendimiento, el sistema cambia a un modo más detallado, lo que baja los FPS y puede provocar un cambio de vuelta — y así en bucle.
Cómo llegamos a la solución
Bien, LOD es genial. Pero su implementación requiere Canvas para el rendimiento, y eso es un nuevo dolor de cabeza. Dibujar en Canvas no es tan difícil — los problemas aparecen cuando hace falta interactividad.
Problema: ¿cómo saber dónde hizo clic el usuario?
En HTML todo es simple: haces clic en un botón — recibes el evento en el elemento. En Canvas es más complicado: haces clic en el lienzo — ¿y ahora qué? Hay que averiguar por cuenta propia en qué elemento hizo clic el usuario.
Básicamente existen tres enfoques:
- Pixel Testing (color picking),
- Enfoque geométrico (recorrer todos los elementos),
- Spatial Indexing (índice espacial).
Pixel Testing (color picking)
La idea es simple: creamos un segundo canvas invisible, copiamos la escena allí, pero rellenamos cada elemento con un color único que actuará como ID del objeto. Al hacer clic leemos el color del píxel bajo el puntero con getImageData y así obtenemos el ID del elemento.
|
Pros |
Contras |
|
|
Para escenas pequeñas sirve, pero con 10.000+ elementos el porcentaje de errores se vuelve inaceptable — descartamos Pixel Testing.
Enfoque geométrico (recorrer todos los elementos)
La idea es simple: recorremos todos los elementos y comprobamos si el punto del clic está dentro del elemento.
|
Pros |
Contras |
|
|
Spatial Indexing
Una evolución del enfoque geométrico. En el enfoque geométrico chocábamos con la cantidad de elementos. Los algoritmos de índice espacial intentan agrupar elementos cercanos usando principalmente árboles, lo que permite reducir la complejidad a log n.
Hay bastantes algoritmos de índice espacial; nosotros elegimos la estructura de datos R‑Tree usando la biblioteca rbush.
R‑Tree es, como indica el nombre, un árbol donde cada objeto se coloca en un rectángulo mínimo (MBR), y luego estos rectángulos se agrupan en rectángulos más grandes. Así se obtiene un árbol donde cada rectángulo contiene otros rectángulos.

Para buscar en un RTree debemos bajar por el árbol (hacia la profundidad del rectángulo) hasta llegar al elemento concreto. La ruta se elige comprobando la intersección del rectángulo de búsqueda con los MBR. Todas las ramas cuyo bounding‑box ni siquiera toca el rectángulo de búsqueda se descartan de inmediato — por eso la profundidad de recorrido suele limitarse a 3–5 niveles, y la búsqueda toma microsegundos incluso con decenas de miles de elementos.
Esta variante, aunque funciona más lento (O (log n) en el mejor caso y O (n) en el peor) que el pixel testing, es más precisa y menos exigente con la memoria.
Modelo de eventos
Con base en el RTree ahora podemos construir nuestro modelo de eventos. Cuando el usuario hace clic, se запуска un hit‑test: formamos un rectángulo de 1×1 píxel en las coordenadas del cursor y buscamos su intersección en el R‑Tree. Al obtener el elemento en el que cae ese rectángulo, delegamos el evento a ese elemento. Si el elemento no detuvo el evento, este se pasa a su padre y así hasta la raíz. El comportamiento de este modelo se parece al modelo de eventos del navegador. Los eventos se pueden interceptar, prevenir o detener su burbujeo.
Como ya mencioné, en el hit‑test formamos un rectángulo de 1×1 píxel, lo que significa que podemos formar un rectángulo de cualquier tamaño. Y eso nos ayudará a hacer otra optimización muy importante — Spatial Culling.
Spatial Culling
Spatial Culling es una técnica de optimización del renderizado cuyo objetivo es no dibujar lo que no se ve. Por ejemplo, no dibujar objetos que están fuera del espacio de la cámara o que están tapados por otros elementos de la escena. Como nuestro grafo se dibuja en un espacio 2D, basta con no dibujar los objetos que están fuera del área visible de la cámara (viewport).
Cómo funciona:
- en cada desplazamiento o zoom de la cámara formamos un rectángulo igual al viewport actual;
- buscamos su intersección en el R‑Tree;
- el resultado es una lista de elementos que realmente son visibles;
- renderizamos solo esos, el resto se omite.
Este enfoque hace que el rendimiento sea casi independiente del número total de elementos: si en el frame caben 40 bloques, la biblioteca dibujará exactamente 40, y no decenas de miles ocultos fuera de la pantalla. En escalas lejanas entran muchos elementos en el viewport, así que dibujamos primitivas ligeras de Canvas; al acercar la cámara, la cantidad de elementos disminuye y los recursos liberados permiten cambiar al modo HTML con detalle completo.
Uniéndolo todo, queda un esquema simple:
- Canvas se encarga de la velocidad,
- HTML — de la interactividad,
- R‑Tree y Spatial Culling los combinan silenciosamente en un sistema único, permitiendo determinar rápidamente qué elementos se pueden dibujar en la capa HTML.
Mientras la cámara se mueve, el viewport pequeño pide al R‑Tree solo los objetos que realmente están en el encuadre. Este enfoque nos permite dibujar grafos realmente grandes o, al menos, tener margen de rendimiento hasta que el usuario limite el viewport.
En resumen, en su núcleo la biblioteca contiene:
- modo Canvas con primitivas simples;
- modo HTML con detalle completo;
- R‑Tree y Spatial Culling para optimización de rendimiento;
- un modelo de eventos familiar.
Pero para producción esto no es suficiente: hace falta poder ampliar la biblioteca y personalizarla para las propias necesidades.
Personalización
La biblioteca ofrece dos formas complementarias de ampliar y cambiar el comportamiento:
- Redefinición de componentes base. Cambiamos la lógica de los Block, Anchor y Connection estándar.
- Extensión mediante capas (Layers). Añadimos funcionalidad fundamentalmente nueva por encima/debajo de la escena existente.
Redefinición de componentes
Cuando hay que modificar el aspecto o comportamiento de elementos existentes, heredamos de la clase base y redefinimos los métodos clave. Luego registramos el componente con nuestro propio nombre.
Personalización de bloques
Por ejemplo, si necesita crear un grafo con barras de progreso en los bloques — digamos, para mostrar el estado de ejecución de tareas en un pipeline — puede personalizar fácilmente los bloques estándar:
import { CanvasBlock } from "@gravity‑ui/graph";
class ProgressBlock extends CanvasBlock {
// Forma base del bloque con esquinas redondeadas
public override renderBody(ctx: CanvasRenderingContext2D): void {
ctx.fillStyle = "#ddd";
ctx.beginPath();
ctx.roundRect(this.state.x, this.state.y, this.state.width, this.state.height, 12);
ctx.fill();
ctx.closePath();
}
public renderSchematicView(ctx: CanvasRenderingContext2D): void {
const progress = this.state.meta?.progress || 0;
// Dibujamos la base del bloque
this.renderBody(ctx);
// Barra de progreso con indicación de color
const progressWidth = (this.state.width - 20) * (progress / 100);
ctx.fillStyle = progress < 50 ? "#ff6b6b" : progress < 80 ? "#feca57" : "#48cae4";
ctx.fillRect(this.state.x + 10, this.state.y + this.state.height - 15, progressWidth, 8);
// Marco de la barra de progreso
ctx.strokeStyle = "#ddd";
ctx.lineWidth = 1;
ctx.strokeRect(this.state.x + 10, this.state.y + this.state.height - 15, this.state.width - 20, 8);
// Texto con porcentajes y nombre
ctx.fillStyle = "#2d3436";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(`${Math.round(progress)}%`, this.state.x + this.state.width / 2, this.state.y + 20);
ctx.fillText(this.state.name, this.state.x + this.state.width / 2, this.state.y + 40);
}
}
Personalización de conexiones
De forma similar, si necesita cambiar el comportamiento y el aspecto de las conexiones — por ejemplo, para mostrar la intensidad del flujo de datos entre bloques — puede crear una conexión personalizada:
import { BlockConnection } from "@gravity-ui/graph";
class DataFlowConnection extends BlockConnection {
public override style(ctx: CanvasRenderingContext2D) {
// Obtenemos datos del flujo desde los bloques conectados
const sourceBlock = this.sourceBlock;
const targetBlock = this.targetBlock;
const sourceProgress = sourceBlock?.state.meta?.progress || 0;
const targetProgress = targetBlock?.state.meta?.progress || 0;
// Calculamos la intensidad del flujo basándonos en el progreso de los bloques
const flowRate = Math.min(sourceProgress, targetProgress);
const isActive = flowRate > 10; // El flujo está activo si el progreso > 10%
if (isActive) {
// Flujo activo -- línea verde gruesa
ctx.strokeStyle = "#00b894";
ctx.lineWidth = Math.max(2, Math.min(6, flowRate / 20));
} else {
// Flujo inactivo -- línea gris discontinua
ctx.strokeStyle = "#ddd";
ctx.lineWidth = this.context.camera.getCameraScale();
ctx.setLineDash([5, 5]);
}
return { type: "stroke" };
}
}
Uso de componentes personalizados
Registramos los componentes creados en la configuración del grafo:
const customGraph = new Graph({
blocks: [
{
id: "task1",
is: "progress",
x: 100,
y: 100,
width: 200,
height: 80,
name: "Data Processing",
meta: { progress: 75 },
},
{
id: "task2",
is: "progress",
x: 400,
y: 100,
width: 200,
height: 80,
name: "Analysis",
meta: { progress: 30 },
},
{
id: "task3",
is: "progress",
x: 700,
y: 100,
width: 200,
height: 80,
name: "Output",
meta: { progress: 5 },
},
],
connections: [
{ sourceBlockId: "task1", targetBlockId: "task2" },
{ sourceBlockId: "task2", targetBlockId: "task3" },
],
settings: {
// Registramos bloques personalizados
blockComponents: {
'progress': ProgressBlock,
},
// Registramos una conexión personalizada para todas las conexiones
connection: DataFlowConnection,
useBezierConnections: true,
},
});
customGraph.setEntities({
blocks: [
{
is: 'progress',
id: '1',
name: "progress block',
x: 10,
y: 10,
width: 10,
height: 10,
anchors: [],
selected: false,
}
]
})
customGraph.start();
Resultado
Como resultado obtenemos un grafo donde:
- los bloques muestran el progreso actual con indicación por color;
- las conexiones visualizan el flujo de datos: flujos activos — verdes y gruesos, inactivos — grises y discontinuos;
- al hacer zoom los bloques cambian automáticamente al modo HTML con interactividad completa.
Extensión mediante capas
Las capas son elementos Canvas o HTML adicionales que se insertan en el “espacio” del grafo. En esencia, cada capa es un canal de renderizado separado que puede contener su propio canvas para gráficos rápidos o un contenedor HTML para elementos interactivos complejos.
Por cierto, así es como funciona la integración con React de nuestra biblioteca: los componentes React se renderizan en la capa HTML a través de React Portal.
Arquitectura de capas
Las capas son otra solución clave al dilema Canvas vs HTML. Sincronizan las posiciones de los elementos Canvas y HTML, garantizando el solapamiento correcto entre ellos. Esto permite cambiar sin fricción entre Canvas y HTML permaneciendo en el mismo espacio. El grafo consta de capas independientes superpuestas:

Las capas pueden trabajar en dos sistemas de coordenadas:
-
Vinculadas al grafo (
transformByCameraPosition: true):- los elementos se mueven junto con la cámara,
- bloques, conexiones, elementos del grafo.
-
Fijadas en la pantalla (
transformByCameraPosition: false):- se quedan en su sitio al hacer panorámica,
- toolbars, leyendas, controles de UI.
Cómo está hecha la integración con React
Una capa con integración React es bastante ilustrativa para mostrar qué son las capas. Primero, veamos un componente que resalta la lista de bloques que están dentro del área visible de la cámara. Para ello debemos suscribirnos a los cambios de cámara y, tras cada cambio, comprobar la intersección del viewport de la cámara con el hitbox de los elementos.
import { Graph } from "@gravity-ui/graph";
const BlocksList = ({ graph, renderBlock }: { graph: Graph, renderBlock: (graph: Graph, block: TBlock) => React.JSX.Element }) => {
const [blocks, setBlocks] = useState([]);
const updateVisibleList = useCallback(() => {
const cameraState = graph.cameraService.getCameraState();
const CAMERA_VIEWPORT_TRESHOLD = 0.5;
const x = -cameraState.relativeX - cameraState.relativeWidth * CAMERA_VIEWPORT_TRESHOLD;
const y = -cameraState.relativeY - cameraState.relativeHeight * CAMERA_VIEWPORT_TRESHOLD;
const width = -cameraState.relativeX + cameraState.relativeWidth * (1 + CAMERA_VIEWPORT_TRESHOLD) - x;
const height = -cameraState.relativeY + cameraState.relativeHeight * (1 + CAMERA_VIEWPORT_TRESHOLD) - y;
const blocks = graph
.getElementsOverRect(
{
x,
y,
width,
height,
}, // define el área en la que se buscará la lista de bloques
[CanvasBlock] // define los tipos de elementos que se buscarán en el área visible de la cámara
).map((component) => component.connectedState); // Obtenemos la lista de modelos de bloques
setBlocks(blocks);
});
useGraphEvent(graph, "camera-change", ({ scale }) => {
if (scale >= 0.7) {
// Si el zoom es mayor que 0.7, actualizamos la lista de bloques
updateVisibleList()
return;
}
setBlocks([]);
});
return blocks.map(block => <React.Fragment key={block.id}>{renderBlock(graphObject, block)}</React.Fragment>)
}
Ahora veamos la descripción de la propia capa que usará este componente.
import { Layer } from '@gravity-ui/graph';
class ReactLayer extends Layer {
constructor(props: TReactLayerProps) {
super({
html: {
zIndex: 3, // elevamos la capa por encima del resto
classNames: ["no-user-select"], // añadimos una clase para desactivar la selección de texto
transformByCameraPosition: true, // la capa está vinculada a la cámara: se moverá junto con ella
},
...props,
});
}
public renderPortal(renderBlock: <T extends TBlock>(block: T) => React.JSX.Element) {
if (!this.getHTML()) {
return null;
}
const htmlLayer = this.getHTML() as HTMLDivElement;
return createPortal(
React.createElement(BlocksList, {
graph: this.context.graph,
renderBlock: renderBlock,
}),
htmlLayer,
);
}
}
Ahora podemos usar esta capa en nuestra aplicación.
import { Flex } from "@gravity-ui/uikit";
const graph = useMemo(() => new Graph());
const containerRef = useRef<HTMLDivElement>();
useEffect(() => {
if (containerRef.current) {
graph.attach(containerRef.current);
}
return () => {
graph.detach();
};
}, [graph, containerRef]);
const reactLayer = useLayer(graph, ReactLayer, {});
const renderBlock = useCallback((graph, block) => <Block graph={graph} block={block}>{block.name}</Block>)
return (
<div>
<div style={{ position: "absolute", overflow: "hidden", width: "100%", height: "100%" }} ref={containerRef}>
{graph && reactLayer && reactLayer.renderPortal(renderBlock)}
</div>
</div>
);
En general, todo es bastante simple. Nada de lo descrito arriba hay que escribirlo uno mismo — ya está implementado y listo para usar.
Nuestra biblioteca de grafos: ventajas y cómo usarla
Cuando empezamos a trabajar en la biblioteca, la pregunta principal era: ¿cómo hacer que el desarrollador no tenga que elegir entre rendimiento y comodidad de desarrollo? La respuesta estuvo en automatizar esa elección.
Ventajas
Rendimiento + comodidad
@gravity‑ui/graph cambia automáticamente entre Canvas y HTML según el nivel de zoom. Esto significa que obtienes:
- 60 FPS estables en grafos con miles de elementos.
- Posibilidad de usar componentes HTML completos con interactividad rica al ver con detalle.
- Un modelo de eventos unificado independientemente del renderizado — click, mouseenter funcionan igual en Canvas y en HTML.
Compatibilidad con bibliotecas de UI
Una de las principales ventajas es la compatibilidad con cualquier biblioteca de UI. Si tu equipo usa:
- Gravity UI,
- Material‑UI,
- Ant Design,
- componentes propios.
… ¡no tienes que renunciar a ellas! Al aumentar el zoom, el grafo cambia automáticamente al modo HTML, donde los Button, Select, DatePicker habituales en el tema de color que necesites funcionan igual que en una aplicación React normal.
Agnóstico de framework
Aunque implementamos el renderer HTML base usando React, intentamos desarrollar la biblioteca de forma que siga siendo agnóstica de framework. Es decir, si hace falta, puedes implementar con relativa facilidad una capa de integración con tu framework favorito.
¿Y hay alternativas?
En el mercado actualmente hay bastantes soluciones para dibujar grafos: desde soluciones de pago como yFiles y JointJS, hasta soluciones open source como Foblex Flow, baklavajs y jsPlumb. Pero para comparar consideramos @antv/g6 y React Flow como las herramientas más populares. Cada una tiene sus particularidades.
React Flow es una buena biblioteca enfocada en construir interfaces node‑based. Tiene muchas capacidades, pero debido a que usa SVG y HTML, el rendimiento es bastante modesto. La biblioteca es buena cuando estás seguro de que los grafos no superarán 100–200 bloques.
Por su parte, @antv/g6 tiene un montón de funcionalidades; soporta Canvas y, en particular, WebGL. Probablemente no se puede comparar directamente @antv/g6 con @gravity‑ui/graph: el equipo está más orientado a grafos y diagramas, aunque también soporta UI node‑based. Así que antv/g6 encaja si te importa no solo la interfaz node‑based, sino también dibujar gráficas/diagramas.
Aunque @antv/g6 sabe tanto canvas/webgl como html/svg, las reglas de conmutación tendrás que gestionarlas a mano, y hay que hacerlo bien. En rendimiento es mucho más rápida que React Flow, pero aún así surgen dudas. Aunque se declara soporte de WebGL, si miras su stress test, se nota que con 60k nodos la biblioteca no puede ofrecer dinamismo — en un MacBook M3 el renderizado de un frame tardó 4 segundos. Para comparar: nuestro stress test con 111k nodos y 109k conexiones en el mismo Macbook M3: renderizar la escena completa del grafo toma ~60ms, lo que da ~15–20 FPS. No es mucho, pero gracias a Spatial Culling se puede limitar el viewport y así mejorar la capacidad de respuesta. Aunque los maintainers declararon que quieren lograr renderizar 100k nodos a 30 FPS, por lo visto aún no lo han conseguido.
Otro punto en el que @gravity‑ui/graph gana es el tamaño del bundle.
|
Bundle size Minified |
Bundle size Minified + Gzipped |
|
|
@antv/g6 bundlephobia |
1.1 MB |
324.5 kB |
|
react flow bundlephobia |
181.2 kB |
56.4 kB |
|
@gravity-ui/graph bundlephobia |
2.2 kB |
672 B |
Aunque ambas bibliotecas son bastante potentes en rendimiento o en facilidad de integración, @gravity‑ui/graph tiene varias ventajas: puede ofrecer rendimiento en grafos realmente grandes, mantener UI/UX para el usuario y simplificar el desarrollo.
Planes para el futuro
Ya ahora la biblioteca tiene suficiente margen de rendimiento para la mayoría de tareas, así que en el futuro cercano prestaremos más atención al desarrollo del ecosistema alrededor de la biblioteca: desarrollaremos capas (plugins), integraciones para otras bibliotecas y frameworks (Angular/Vue/Svelte, …etc), añadiremos soporte para dispositivos táctiles, adaptación para navegadores móviles y, en general, mejoraremos UX/DX.
Pruébalo y únete
En el repositorio encontrarás una biblioteca totalmente funcional:
- Núcleo en Canvas + R‑Tree (≈ 30K líneas de código),
- Integración con React,
- Storybook con ejemplos.
Puedes instalar la biblioteca en una sola línea:
npm install @gravity-ui/graph
Durante bastante tiempo, la biblioteca que hoy se llama @gravity‑ui/graph fue una herramienta interna dentro de Nirvana, y el enfoque elegido se probó muy bien en la práctica. Ahora queremos compartir nuestro trabajo y ayudar a desarrolladores externos a dibujar sus grafos de forma más simple, rápida y eficiente.
Queremos estandarizar los enfoques para mostrar grafos complejos en la comunidad open source: demasiados equipos reinventan la rueda o sufren con herramientas inadecuadas.
Por eso es muy importante para nosotros reunir tu feedback: distintos proyectos traen distintos edge cases que permiten mejorar la biblioteca. Esto nos ayudará a pulirla y a hacer crecer más rápido el ecosistema de Gravity UI.

Andréi Shchetinin
Desarrollador senior de interfaces