Image de Wikipédia R‑tree

Bibliothèque de visualisation de graphes: comment nous avons résolu le dilemme Canvas vs HTML
Bibliothèque de visualisation de graphes: comment nous avons résolu le dilemme Canvas vs HTML
Bonjour! Je m’appelle Andreï et je suis développeur d’interfaces dans l’équipe User Experience des services d’infrastructure de Yandex. Nous développons Gravity UI — un design system open source et une bibliothèque de composants React, utilisés par des dizaines de produits au sein de l’entreprise et au-delà. Aujourd’hui, je vais raconter comment nous avons été confrontés à la visualisation de graphes complexes, pourquoi les solutions existantes ne nous convenaient pas, et comment @gravity‑ui/graph est finalement née — une bibliothèque que nous avons décidé d’ouvrir à la communauté.
Cette histoire a commencé par un problème très concret: nous devions rendre des graphes de plus de 10 000 éléments avec des composants interactifs. Chez Yandex, il existe de nombreux projets où les utilisateurs construisent des pipelines complexes de traitement de données — des simples processus ETL jusqu’au machine learning. Quand ces pipelines sont générés de manière programmatique, le nombre de blocs peut atteindre des dizaines de milliers.
Les solutions existantes ne nous satisfaisaient pas:
- Les bibliothèques HTML/SVG sont belles et agréables à développer, mais commencent à ralentir dès quelques centaines d’éléments.
- Les solutions Canvas tiennent la performance, mais exigent énormément de code pour construire des éléments UI complexes.
Dessiner un bouton aux coins arrondis avec un dégradé dans Canvas n’est pas difficile. Les problèmes apparaissent lorsqu’il faut créer des contrôles complexes ou une mise en page — il faut écrire des dizaines de lignes de commandes de dessin bas niveau. Chaque élément d’interface doit être programmé de zéro — de la gestion des clics aux animations. Or, nous avions besoin de composants UI complets: boutons, selects, champs de saisie, drag-and-drop.
Nous avons décidé de ne pas choisir entre Canvas et HTML, mais d’utiliser le meilleur des deux technologies. L’idée était simple: basculer automatiquement entre les modes selon le niveau de zoom de l’utilisateur sur le graphe.

Essayez par vous-même
D’où vient le besoin
Nirvana et ses graphes
Chez Yandex, nous avons le service Nirvana pour créer et exécuter des graphes de traitement de données (nous en avions parlé dès 2018). C’est un service grand, populaire, qui existe depuis longtemps.
Une partie des utilisateurs crée les graphes à la main — à la souris, en ajoutant des blocs et en les reliant. Avec ces graphes, pas de souci: il y a peu de blocs et tout fonctionne parfaitement. Mais il existe des projets qui génèrent les graphes automatiquement. Et là, les difficultés commencent: ils peuvent mettre jusqu’à 10 000 opérations dans un seul graphe. Cela donne quelque chose comme ça:

Et aussi comme ça:





De tels graphes, une simple combinaison HTML + SVG ne les supporte pas. Le navigateur commence à ramer, la mémoire fuit, l’utilisateur souffre. Nous avons tenté une approche frontale: optimiser le rendu HTML, mais tôt ou tard nous butions sur des limites physiques — le DOM n’est tout simplement pas conçu pour des milliers d’éléments interactifs flottants visibles simultanément.
Il fallait une autre solution, et dans le navigateur il ne nous restait que Canvas. Lui seul peut fournir la performance nécessaire.
Première idée: trouver une solution prête à l’emploi. Nous étions en 2017–2018, et nous avons passé au crible des bibliothèques populaires pour Canvas ou le rendu de graphes, mais toutes se heurtaient au même problème: soit Canvas avec des éléments primitifs, soit HTML/SVG en sacrifiant les performances.
Et si on ne choisissait pas?
Level of Details: inspiration du GameDev
Dans le GameDev et la cartographie, il existe un concept très efficace: Level of Details (LOD). Cette technique est née d’une nécessité: comment afficher un monde immense sans tuer les performances?
Le principe est simple: un même objet peut avoir plusieurs niveaux de détail selon la distance d’observation. Dans les jeux, c’est particulièrement visible:
- Au loin, on voit des montagnes — des polygones simples avec une texture de base.
- En s’approchant, des détails apparaissent: herbe, rochers, ombres.
- Encore plus près, on distingue des feuilles individuelles sur les arbres.
Personne ne rend des millions de polygones d’herbe quand le joueur est au sommet d’une montagne et regarde au loin.
Sur les cartes, le principe est le même — à chaque niveau de zoom correspond un jeu de données et un niveau de détail:
- À l’échelle d’un continent — seules les frontières des pays.
- En zoomant sur une ville — rues et quartiers apparaissent.
- Encore plus près — numéros, cafés, arrêts de bus.
Nous avons compris: l’utilisateur n’a pas besoin de boutons interactifs à grande échelle sur un graphe de 10 000 blocs — il ne les verra pas et ne pourra pas interagir avec eux.
De plus, tenter de rendre 10 000 éléments HTML simultanément figera le navigateur. Mais quand il zoome sur une zone précise, le nombre de blocs visibles chute fortement — de 10 000 à, disons, 50. C’est précisément là que des ressources se libèrent pour des composants HTML riches en interactivité.
Trois niveaux dans notre schéma Level of Details
Minimalistic (zoom 0,1–0,3) — Canvas avec des primitives simples
Dans ce mode, l’utilisateur voit l’architecture globale du système: où se trouvent les principaux groupes de blocs et comment ils sont reliés. Chaque bloc est un simple rectangle avec un codage couleur de base. Aucun texte, bouton ou icône détaillée. En revanche, on peut rendre confortablement des milliers d’éléments. À ce niveau, l’utilisateur choisit la zone à étudier en détail.

Schematic (zoom 0,3–0,7) — Canvas avec des détails
Les noms des blocs, des icônes d’état et des ancres de connexion apparaissent. Le texte est rendu via l’API Canvas — c’est rapide, mais les possibilités de style sont limitées. Les liens entre les blocs deviennent plus informatifs: on peut montrer la direction du flux de données, le statut de la connexion. C’est un mode de transition où la performance de Canvas se combine à une information de base.

Detailed (zoom 0,7+) — HTML avec interactivité complète
Ici, les blocs se transforment en véritables composants d’interface: avec des boutons de contrôle, des champs de paramètres, des barres de progression, des selects. On peut utiliser tout ce que permet HTML/CSS et intégrer des bibliothèques UI. Dans ce mode, le viewport contient généralement au plus 20–50 blocs — idéal pour un travail détaillé.

Et si on utilisait le FPS pour choisir le niveau de détail?
Nous avons envisagé des approches où le niveau de détail dépendait du FPS. Mais nous avons constaté que cela rend le système instable: quand les performances augmentent, le système passe à un mode plus détaillé, ce qui baisse le FPS et peut provoquer un retour en arrière — et ainsi de suite en boucle.
Comment nous sommes arrivés à la solution
D’accord, le LOD, c’est excellent. Mais l’implémentation nécessite Canvas pour la performance, et c’est un nouveau casse-tête. Dessiner sur Canvas n’est pas très difficile — les problèmes apparaissent quand il faut de l’interactivité.
Problème: comment savoir où l’utilisateur a cliqué?
En HTML, tout est simple: vous cliquez sur un bouton — l’événement arrive directement sur l’élément. Avec Canvas, c’est plus compliqué: vous cliquez sur le canvas — et ensuite? Il faut déterminer soi-même sur quel élément l’utilisateur a cliqué.
Il existe essentiellement trois approches:
- Pixel Testing (color picking),
- Approche géométrique (parcours naïf de tous les éléments),
- Spatial Indexing (index spatial).
Pixel Testing (color picking)
L’idée est simple: on crée un deuxième canvas invisible, on y copie la scène, mais on remplit chaque élément avec une couleur unique qui servira d’ID. Au clic, on lit la couleur du pixel sous le pointeur via getImageData et on obtient l’ID de l’élément.
|
Avantages |
Inconvénients |
|
|
Pour des petites scènes, la méthode convient, mais avec 10 000+ éléments, le taux d’erreur devient inacceptable — on met Pixel Testing de côté.
Approche géométrique (parcours naïf de tous les éléments)
L’idée est simple: on parcourt tous les éléments et on vérifie si le point du clic se trouve à l’intérieur de l’élément.
|
Avantages |
Inconvénients |
|
|
Spatial Indexing
Une évolution de l’approche géométrique. Dans l’approche géométrique, on butait sur le nombre d’éléments. Les algorithmes d’index spatial tentent de regrouper des éléments proches, principalement via des arbres, ce qui permet de réduire la complexité à log n.
Il existe de nombreux algorithmes d’index spatial; nous avons choisi la structure R‑Tree via la bibliothèque rbush.
Un R‑Tree est, comme son nom l’indique, un arbre où chaque objet est placé dans un rectangle minimal (MBR), puis ces rectangles sont regroupés en rectangles plus grands. On obtient ainsi un arbre où chaque rectangle contient d’autres rectangles.

Pour chercher dans un RTree, il faut descendre dans l’arbre (en profondeur du rectangle) jusqu’à atteindre l’élément précis. Le chemin est choisi en testant l’intersection du rectangle de recherche avec les MBR. Toutes les branches dont la bounding‑box ne touche même pas le rectangle de recherche sont éliminées immédiatement — c’est pourquoi la profondeur de parcours est généralement limitée à 3–5 niveaux, et la recherche prend des microsecondes même avec des dizaines de milliers d’éléments.
Cette variante est un peu plus lente (O (log n) dans le meilleur cas et O (n) dans le pire) que le pixel testing, mais elle est plus précise et moins gourmande en mémoire.
Modèle d’événements
À partir du RTree, on peut construire notre modèle d’événements. Quand l’utilisateur clique, on lance un hit test: on forme un rectangle 1×1 pixel aux coordonnées du curseur et on cherche son intersection dans le R‑Tree. Une fois l’élément trouvé, on lui délègue l’événement. Si l’élément ne stoppe pas l’événement, celui-ci est transmis à son parent, et ainsi de suite jusqu’à la racine. Le comportement est proche du modèle d’événements du navigateur. Les événements peuvent être interceptés, prevent (preventDefault) ou arrêter la propagation.
Comme je l’ai mentionné, lors du hit test nous formons un rectangle 1×1 pixel, ce qui signifie que nous pouvons former un rectangle de n’importe quelle taille. Cela nous aidera à réaliser une autre optimisation très importante — le Spatial Culling.
Spatial Culling
Le Spatial Culling est une technique d’optimisation du rendu visant à ne pas dessiner ce qui n’est pas visible. Par exemple, ne pas dessiner des objets hors du champ de la caméra ou masqués par d’autres éléments. Comme notre graphe est rendu en 2D, il suffit de ne pas dessiner les objets situés en dehors du viewport.
Fonctionnement:
- à chaque déplacement ou zoom de la caméra, on forme un rectangle égal au viewport courant;
- on cherche son intersection dans le R‑Tree;
- on obtient la liste des éléments réellement visibles;
- on ne rend que ceux-ci, le reste est ignoré.
Cette technique rend la performance presque indépendante du nombre total d’éléments: si 40 blocs tiennent dans le cadre, la bibliothèque dessinera exactement 40, et non des dizaines de milliers hors écran. À grande échelle, beaucoup d’éléments tombent dans le viewport, donc on dessine des primitives Canvas légères; en zoomant, le nombre d’éléments diminue, et les ressources libérées permettent de basculer en mode HTML avec un niveau de détail complet.
En rassemblant tout, on obtient un schéma simple:
- Canvas assure la vitesse,
- HTML — l’interactivité,
- R‑Tree et Spatial Culling les unifient discrètement en un seul système, permettant de déterminer rapidement quels éléments peuvent être dessinés sur la couche HTML.
Tant que la caméra bouge, le petit viewport ne demande au R‑Tree que les objets réellement présents à l’écran. Cette approche nous permet de dessiner des graphes vraiment grands, ou au moins de conserver une marge de performance tant que l’utilisateur n’a pas restreint le viewport.
Au final, le cœur de la bibliothèque contient:
- un mode Canvas avec des primitives simples;
- un mode HTML avec un détail complet;
- R‑Tree et Spatial Culling pour optimiser les performances;
- un modèle d’événements familier.
Mais pour de la production, ce n’est pas suffisant: il faut pouvoir étendre la bibliothèque et la personnaliser selon ses besoins.
Personnalisation
La bibliothèque propose deux moyens complémentaires d’extension et de modification du comportement:
- Redéfinition des composants de base. On change la logique des Block, Anchor et Connection standards.
- Extension via des couches (Layers). On ajoute des fonctionnalités fondamentalement nouvelles au-dessus/en dessous de la scène existante.
Redéfinition des composants
Lorsque vous devez modifier l’apparence ou le comportement d’éléments existants, vous héritez de la classe de base et redéfinissez les méthodes clés. Ensuite, vous enregistrez le composant sous votre propre nom.
Personnalisation des blocs
Par exemple, si vous devez créer un graphe avec des barres de progression sur les blocs — pour afficher l’état d’exécution des tâches dans un pipeline — vous pouvez facilement personnaliser les blocs standards:
import { CanvasBlock } from "@gravity‑ui/graph";
class ProgressBlock extends CanvasBlock {
// Forme de base du bloc avec coins arrondis
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;
// Dessiner la base du bloc
this.renderBody(ctx);
// Barre de progression avec indication couleur
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);
// Cadre de la barre de progression
ctx.strokeStyle = "#ddd";
ctx.lineWidth = 1;
ctx.strokeRect(this.state.x + 10, this.state.y + this.state.height - 15, this.state.width - 20, 8);
// Texte avec pourcentage et nom
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);
}
}
Personnalisation des connexions
De même, si vous devez modifier le comportement et l’apparence des liens — par exemple pour montrer l’intensité du flux de données entre les blocs — vous pouvez créer une connexion personnalisée:
import { BlockConnection } from "@gravity-ui/graph";
class DataFlowConnection extends BlockConnection {
public override style(ctx: CanvasRenderingContext2D) {
// Récupérer des données de flux depuis les blocs liés
const sourceBlock = this.sourceBlock;
const targetBlock = this.targetBlock;
const sourceProgress = sourceBlock?.state.meta?.progress || 0;
const targetProgress = targetBlock?.state.meta?.progress || 0;
// Calculer l’intensité du flux selon la progression des blocs
const flowRate = Math.min(sourceProgress, targetProgress);
const isActive = flowRate > 10; // Flux actif si progression > 10%
if (isActive) {
// Flux actif -- ligne verte épaisse
ctx.strokeStyle = "#00b894";
ctx.lineWidth = Math.max(2, Math.min(6, flowRate / 20));
} else {
// Flux inactif -- ligne grise en pointillés
ctx.strokeStyle = "#ddd";
ctx.lineWidth = this.context.camera.getCameraScale();
ctx.setLineDash([5, 5]);
}
return { type: "stroke" };
}
}
Utilisation de composants personnalisés
On enregistre les composants créés dans la configuration du graphe:
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: {
// Enregistrer des blocs personnalisés
blockComponents: {
'progress': ProgressBlock,
},
// Enregistrer une connexion personnalisée pour tous les liens
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();
Résultat
Au final, on obtient un graphe où:
- les blocs affichent la progression actuelle avec un code couleur;
- les connexions visualisent le flux de données: flux actifs — verts et épais, flux inactifs — gris et en pointillés;
- au zoom, les blocs basculent automatiquement en mode HTML avec interactivité complète.
Extension via des couches
Les couches (layers) sont des éléments Canvas ou HTML supplémentaires insérés dans « l’espace » du graphe. En pratique, chaque couche est un canal de rendu distinct pouvant contenir son propre canvas pour des graphismes rapides ou un conteneur HTML pour des éléments interactifs complexes.
D’ailleurs, c’est via les couches que fonctionne l’intégration React de notre bibliothèque: les composants React sont rendus dans la couche HTML via React Portal.
Architecture des couches
Les couches sont une autre réponse clé au dilemme Canvas vs HTML. Elles synchronisent les positions des éléments Canvas et HTML, garantissant leur superposition correcte. Cela permet de basculer de manière fluide entre Canvas et HTML tout en restant dans un espace unique. Le graphe est composé de couches indépendantes superposées:

Les couches peuvent fonctionner dans deux systèmes de coordonnées:
-
Liées au graphe (
transformByCameraPosition: true):- les éléments bougent avec la caméra,
- blocs, connexions, éléments du graphe.
-
Fixées à l’écran (
transformByCameraPosition: false):- restent en place lors du panoramique,
- barres d’outils, légendes, contrôles UI.
Comment l’intégration React est construite
Une couche avec intégration React illustre bien ce que sont les couches. Commençons par un composant qui met en évidence la liste des blocs situés dans la zone visible de la caméra. Pour cela, on doit s’abonner aux changements de caméra et, après chaque changement, vérifier l’intersection du viewport de la caméra avec la hitbox des éléments.
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,
}, // définit la zone dans laquelle on cherchera la liste des blocs
[CanvasBlock] // définit les types d’éléments recherchés dans la zone visible de la caméra
).map((component) => component.connectedState); // obtenir la liste des modèles de blocs
setBlocks(blocks);
});
useGraphEvent(graph, "camera-change", ({ scale }) => {
if (scale >= 0.7) {
// Si l’échelle est > 0.7, on met à jour la liste des blocs
updateVisibleList()
return;
}
setBlocks([]);
});
return blocks.map(block => <React.Fragment key={block.id}>{renderBlock(graphObject, block)}</React.Fragment>)
}
Maintenant, regardons la définition de la couche elle-même qui utilisera ce composant.
import { Layer } from '@gravity-ui/graph';
class ReactLayer extends Layer {
constructor(props: TReactLayerProps) {
super({
html: {
zIndex: 3, // placer la couche au-dessus des autres
classNames: ["no-user-select"], // ajouter une classe pour désactiver la sélection de texte
transformByCameraPosition: true, // couche liée à la caméra : elle bouge avec la caméra
},
...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,
);
}
}
Nous pouvons maintenant utiliser cette couche dans notre application.
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>
);
Globalement, tout est assez simple. Rien de ce qui a été décrit ci-dessus n’a besoin d’être réécrit: tout est déjà implémenté et prêt à l’emploi.
Notre bibliothèque de graphes: avantages et utilisation
Quand nous avons commencé à travailler sur la bibliothèque, la question principale était: comment faire en sorte que le développeur n’ait pas à choisir entre performance et confort de développement? La réponse a été d’automatiser ce choix.
Avantages
Performance + confort
@gravity‑ui/graph bascule automatiquement entre Canvas et HTML en fonction du zoom. Cela signifie:
- 60 FPS stables sur des graphes de milliers d’éléments.
- Possibilité d’utiliser de véritables composants HTML riches en interactivité lors d’une vue détaillée.
- Un modèle d’événements unique quel que soit le rendu — click, mouseenter fonctionnent de la même façon sur Canvas et en HTML.
Compatibilité avec les bibliothèques UI
L’un des principaux avantages est la compatibilité avec n’importe quelle bibliothèque UI. Si votre équipe utilise:
- Gravity UI,
- Material‑UI,
- Ant Design,
- des composants custom.
… alors vous n’avez pas à y renoncer! En zoomant, le graphe passe automatiquement en mode HTML, où les Button, Select, DatePicker habituels, avec votre thème de couleurs, fonctionnent exactement comme dans une application React classique.
Agnostique au framework
Bien que nous ayons implémenté le renderer HTML de base avec React, nous avons conçu la bibliothèque pour rester agnostique au framework. Cela signifie qu’en cas de besoin, vous pouvez assez simplement implémenter une couche d’intégration pour votre framework préféré.
Existe-t-il des alternatives?
Il existe aujourd’hui beaucoup de solutions pour dessiner des graphes — des solutions payantes comme yFiles et JointJS, jusqu’aux solutions open source Foblex Flow, baklavajs et jsPlumb. Pour la comparaison, nous retenons @antv/g6 et React Flow comme les outils les plus populaires. Chacun a ses particularités.
React Flow est une bonne bibliothèque orientée vers la construction d’interfaces node‑based. Elle offre de nombreuses fonctionnalités, mais à cause de l’utilisation de SVG et HTML, les performances restent assez modestes. Elle est adaptée lorsqu’on est certain que les graphes ne dépasseront pas 100–200 blocs.
De son côté, @antv/g6 dispose d’un grand nombre de fonctionnalités; elle supporte Canvas et notamment WebGL. Comparer directement @antv/g6 et @gravity‑ui/graph n’est probablement pas tout à fait juste: l’équipe est davantage orientée graphes et diagrammes, même si le node‑based UI est aussi supporté. antv/g6 est donc un bon choix si vous avez besoin non seulement d’un node‑based UI, mais aussi de dessiner des graphiques/diagrammes.
Même si @antv/g6 sait faire canvas/webgl et html/svg, il faut gérer soi‑même les règles de bascule, et les faire correctement. En performance, elle est bien plus rapide que React Flow, mais elle soulève tout de même des questions. Bien que le support WebGL soit annoncé, leur stress test montre qu’à 60k nœuds, la bibliothèque n’assure pas une dynamique fluide — sur un MacBook M3, le rendu d’une image a pris 4 secondes. À titre de comparaison, notre stress test avec 111k nœuds et 109k connexions sur le même MacBook M3: le rendu de la scène du graphe entier prend ~60ms, soit ~15–20 FPS. Ce n’est pas énorme, mais grâce au Spatial Culling, on peut restreindre le viewport et ainsi améliorer la réactivité. Même si les mainteneurs avaient indiqué vouloir atteindre 30 FPS pour 100k nœuds, il semble qu’ils n’y soient pas encore parvenus.
Un autre point où @gravity‑ui/graph l’emporte: la taille du 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 |
Bien que ces deux bibliothèques soient très solides en performance ou en facilité d’intégration, @gravity‑ui/graph présente plusieurs avantages: elle peut assurer des performances sur des graphes vraiment volumineux tout en préservant l’UI/UX côté utilisateur et en simplifiant le développement.
Plans pour l’avenir
La bibliothèque dispose déjà d’une marge de performance suffisante pour la plupart des cas. Dans un avenir proche, nous allons donc davantage nous concentrer sur le développement de l’écosystème autour de la bibliothèque: création de couches (plugins), intégrations pour d’autres bibliothèques et frameworks (Angular/Vue/Svelte, …etc), ajout du support des appareils tactiles, adaptation aux navigateurs mobiles et amélioration globale de l’UX/DX.
Essayez et rejoignez-nous
Dans le dépôt, vous trouverez une bibliothèque entièrement fonctionnelle:
- un cœur Canvas + R‑Tree (≈ 30k lignes de code),
- une intégration React,
- un Storybook avec des exemples.
Installer la bibliothèque se fait en une seule ligne:
npm install @gravity-ui/graph
Pendant longtemps, la bibliothèque qui s’appelle aujourd’hui @gravity‑ui/graph était un outil interne de Nirvana, et l’approche choisie a fait ses preuves. Nous souhaitons maintenant partager notre travail et aider les développeurs externes à dessiner leurs graphes plus simplement, plus vite et de manière plus performante.
Nous voulons standardiser les approches d’affichage de graphes complexes dans la communauté open source — trop d’équipes réinventent la roue ou souffrent avec des outils inadaptés.
C’est pourquoi votre feedback est très important: des projets différents apportent des cas limites différents, qui aident à faire évoluer la bibliothèque. Cela nous permettra de l’améliorer et d’accélérer la croissance de l’écosystème Gravity UI.

Andreï Chtchetinine
Développeur senior d’interfaces