Parser, rendre et diffuser de grands JSON
L’essentiel : Le parsing n'est souvent pas le goulot d'étranglement ; le rendu et les copies le sont. Optimisez selon les quatre couches « parse → parcours → rendu → transport » pour que votre effort ne soit pas gaspillé.

L'essentiel : mesurez séparément le parsing, le parcours, le rendu et le transport d'abord, puis travaillez dans cet ordre — réduisez les copies, développez à la demande avec le défilement virtuel, déplacez le travail dans un Worker, passez au flux, et seulement ensuite changez le format. Le flux ne vaut le coup que lorsque le fichier ne tient pas en mémoire ou qu'il faut un petit sous-ensemble de ses champs.
La conclusion « JSON.parse est lent » est fausse huit fois sur dix. Ce qui est réellement lent, c'est ce que vous faites après le parsing.
1. Mesurez d'abord, optimisez ensuite
Pour un même fichier JSON de 10 Mo, le temps passé dans différentes étapes diffère de plusieurs ordres de grandeur :
| Étape | Durée typique | Coût principal |
|---|---|---|
JSON.parse (natif) |
De quelques dizaines à un peu plus de cent millisecondes | Un seul balayage linéaire plus construction d'objets |
Copie profonde (structuredClone / récursive) |
Possiblement 1 à 3 fois le parsing | Reconstruction de tout le graphe d'objets |
| Comparaison profonde / diff | Possiblement le double encore | Parcours récursif plus comparaison de chaînes |
| Rendu dans un arbre DOM | Fige dès qu'il y a des dizaines de milliers de nœuds | Layout et reflow |
| Concaténation de chaînes puis sérialisation | Proportionnel à la taille de sortie | Double la mémoire |
Donc mesurez avant d'agir : dans le navigateur, enveloppez chaque étape avec performance.now(), ou utilisez console.time ;
dans Node, utilisez process.memoryUsage() pour surveiller la croissance du tas. Pas de référence, pas d'optimisation.
2. Couche parsing : trois stratégies
2.1 Parsing complet (le choix par défaut)
const data = JSON.parse(text);
L'implémentation native fait le balayage et la construction d'objets en C++, donc une bibliothèque pure JS peut difficilement la battre. Utilisez-la tant que ces trois conditions tiennent :
- Le texte tient entièrement en mémoire (généralement < 100 Mo) ;
- La plupart des champs sont réellement nécessaires après le parsing ;
- Le degré de non-fiabilité des données est gérable.
2.2 Parsing incrémental en flux
Avec un parseur de style SAX comme stream-json, vous pouvez extraire les champs cibles sans construire d'arbre d'objets complet :
import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';
const pipeline = createReadStream('huge.json')
.pipe(parser())
.pipe(pick({ filter: 'items' }))
.pipe(streamArray());
for await (const { value } of pipeline) {
// Process one element at a time; memory is independent of element count
handleItem(value);
}
Coûts :
- La complexité du code augmente nettement et la gestion d'erreurs devient plus confuse ;
- Seul l'accès séquentiel est possible, sans accès aléatoire ;
- C'est peu convivial pour les besoins d'agrégation globale.
Règle de décision : le flux ne vaut le coup que lorsque le fichier est trop gros pour tenir, ou qu'il faut seulement un très petit sous-ensemble de ses champs.
2.3 Changer le format
Si les données sont « un lot d'enregistrements » plutôt que « un document unique », vous auriez dû utiliser JSON Lines dès le départ. Un format délimité par lignes transforme le « flux » en « lecture ligne par ligne », ce qui abaisse d'un ordre de grandeur la complexité d'implémentation.
3. Couche parcours : une copie de moins gagne la moitié du combat
// Anti-pattern: deep copying first "for safety"
const safeCopy = structuredClone(bigData); // Memory doubles immediately
// Good pattern: pass references directly in read-only scenarios
function renderSummary(data: OrderDocument): Summary {
return { total: data.items.length, amount: data.total };
}
Copies cachées courantes :
JSON.parse(JSON.stringify(x))utilisé comme « copie profonde » — c'est le type le plus coûteux ;{ ...data }ne copie qu'un niveau, mais pour un grand objet il copie quand même le tableau de références de toutes les clés de premier niveau ;Array.prototype.mapcrée un nouveau tableau, ce qui coûte aussi cher quand il y a beaucoup d'éléments ;- Les proxies de réactivité de framework (le
reactivede Vue) proxifient récursivement les grands objets, et la surcharge peut dépasser le parsing lui-même.
Conseil pratique : gardez les « données brutes » comme un objet immuable ordinaire et ne les enveloppez pas dans la réactivité ; calculez et mettez en cache séparément les données dérivées dont vous avez besoin pour l'affichage.
4. Couche rendu : ne fourrez pas tout l'arbre dans le DOM
Un fichier JSON de 5 Mo a environ 30 000 à 100 000 nœuds ; les rendre tous comme éléments DOM fera cesser de répondre le navigateur.
Trois approches viables :
4.1 Développer à la demande
Ne rendez que les enfants directs du nœud racine, et ne calculez le niveau suivant que lorsque le nœud est développé. C'est l'approche standard des visualiseurs d'arbre :
type TreeNode = { key: string; value: unknown; expanded: boolean };
/**
* Returns only the currently visible nodes, avoiding building the whole tree at once.
* @param rootNode Root node
* @param expandedPaths Set of paths that have been expanded
* @returns List of visible nodes
*/
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
const visibleNodes: TreeNode[] = [];
/**
* Recursively collects the child nodes of expanded branches.
* @param currentNode Current node
* @param currentPath Current path
*/
const walk = (currentNode: TreeNode, currentPath: string): void => {
visibleNodes.push(currentNode);
if (!expandedPaths.has(currentPath) || typeof currentNode.value !== 'object' || currentNode.value === null) {
return;
}
for (const [childKey, childValue] of Object.entries(currentNode.value)) {
walk({ key: childKey, value: childValue, expanded: false }, `${currentPath}/${childKey}`);
}
};
walk(rootNode, '');
return visibleNodes;
}
4.2 Défilement virtuel
Une fois les nœuds développés au-delà de quelques centaines, ne rendez que les quelques dizaines à l'intérieur de la fenêtre et réservez l'espace du reste avec des hauteurs fictives. C'est le plus efficace sur les vues de type liste (tableaux, journaux).
4.3 N'afficher que des résumés
Présentez d'abord « structure + type + nombre » (par exemple items: Array(12000)) ; ce n'est que lorsque l'utilisateur ouvre un élément que vous récupérez la valeur concrète.
Cette astuce convient le mieux au scénario « je veux juste confirmer que les champs sont corrects ».
5. Thread principal : déplacez le travail dans un Worker quand il le faut
Le parsing, le formatage et le diff de tout ce qui dépasse 1 Mo devraient aller dans un Web Worker :
// json.worker.ts
self.onmessage = (event: MessageEvent<{ text: string }>) => {
try {
const parsed = JSON.parse(event.data.text);
self.postMessage({ ok: true, data: parsed });
} catch (parseError) {
self.postMessage({ ok: false, message: (parseError as Error).message });
}
};
/**
* Parses JSON inside a Worker so the main thread is not blocked.
* @param text JSON text
* @returns Parse result
*/
function parseInWorker(text: string): Promise<unknown> {
return new Promise((resolvePromise, rejectPromise) => {
const worker = new Worker(new URL('./json.worker.ts', import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
worker.terminate();
if (event.data.ok) {
resolvePromise(event.data.data);
} else {
rejectPromise(new Error(event.data.message));
}
};
worker.postMessage({ text });
});
}
Attention au coût de communication des Workers : postMessage utilise le clonage structuré par défaut, et cloner un objet de 10 Mo une fois prend aussi des dizaines de millisecondes.
Si tout ce qu'il faut est « le parser et le remettre au thread principal », faites le gros travail à l'intérieur du Worker (parse + transformation + validation) et renvoyez un résumé de résultat.
6. Couche transport : optimisation de taille
| Technique | Gain typique | Coût |
|---|---|---|
| gzip / brotli | La sortie compressée fait souvent 10 % à 25 % de la taille originale | Nécessite un support serveur |
| Suppression de l'indentation et des espaces | Réduction de 20 % à 40 % | Non lisible par l'humain |
| Abréviation des noms de champs | Gains notables avec une forte imbrication | Lisibilité réduite ; nécessite une table de correspondance |
| Nombres au lieu de chaînes d'énumération | Faible | Nécessite un dictionnaire |
| Pagination / champs à la demande | Le plus grand | Refonte de l'API |
Pour les pages destinées aux humains, la bonne combinaison est : l'API envoie du JSON compact et compressé, et l'interface l'embellit seulement pour l'affichage. Beaucoup de sites d'outils envoient par défaut du texte embelli au frontend, expédiant 30 % d'octets en plus pour rien.
7. Comment mesurer
/**
* Measures the median duration of a synchronous routine so a single jittery run does not skew the result.
* @param runOnce Function under test
* @param repeatCount Number of repetitions
* @returns Median duration in milliseconds
*/
function measureMedian(runOnce: () => void, repeatCount = 9): number {
const durations: number[] = [];
for (let roundIndex = 0; roundIndex < repeatCount; roundIndex += 1) {
const startedAt = performance.now();
runOnce();
durations.push(performance.now() - startedAt);
}
durations.sort((firstValue, secondValue) => firstValue - secondValue);
return durations[Math.floor(durations.length / 2)];
}
Trois règles de discipline :
- Utilisez un échantillon fixe et ne générez jamais de données au hasard, sinon les chiffres ne sont pas comparables ;
- Exécutez au moins 9 fois et prenez la médiane ; la première exécution inclut le préchauffage JIT et doit être écartée ;
- Notez l'environnement d'exécution (version Node, CPU), sinon les comparaisons entre machines n'ont aucun sens.
8. Liste de vérification
- [ ] Des chiffres de référence existent pour pouvoir comparer avant et après ;
- [ ] Le parsing de tout ce qui dépasse 1 Mo s'exécute dans un Worker ;
- [ ] Les vues arborescentes se développent à la demande et les listes utilisent le défilement virtuel ;
- [ ] Aucune copie profonde ni enveloppe réactive inutile ;
- [ ] JSON Lines est utilisé quand les données sont « un lot d'enregistrements » ;
- [ ] L'API envoie du JSON compact et le formatage ne sert qu'à l'affichage.