Benchmarks de débit
Les six tests canoniques TechEmpower (sérialisation JSON, texte brut, requête de base de données unique, requêtes multiples, Fortunes et mises à jour de données) s'exécutent sur bext et quatre frameworks Node sur le même ensemble de données et la même charge de travail. Chaque réponse n'est pas mise en cache / force-dynamic : un vrai rendu V8 à chaque requête, pas de mise en cache des réponses complètes.
Matériel et méthode. Boîtier de production partagé à 24 cœurs, bouclage, générateur de charge oha, pic de requêtes/s dans la simultanéité {64, 256}, meilleur des exécutions répétées, garanti 100 % de réussite. La boîte est partagée, donc les nombres absolus bougent avec la charge — les ratios sont stables. Chaque framework interroge le même ensemble de données SQLite (Monde 10 000 lignes + 12 fortunes). Reproduire avec harness/tfb-bench.mjs.
Travailleur sérialisé unique par rapport à la valeur par défaut performante#
Les travailleurs de rendu de bext peuvent exécuter sérialisé (un rendu en cours sur un travailleur, le plancher conservateur par cœur) ou en tant que pool multiplexé avec lequel il est livré (réponses des travailleurs démultiplexées par identifiant de demande, attendues directement sur le travailleur HTTP). La valeur par défaut est la piscine.
| Test | 1 travailleur sérialisé | Performance par défaut (pool de 4 travailleurs) |
|---|---|---|
| JSON | 6,419 | 99,174 |
| Texte brut | 7,060 | 102,265 |
| Requête unique | 5,948 | 100,183 |
| Requêtes multiples (20) | 4,023 | 84,945 |
| Fortunes | 4,519 | 95,296 |
| Mises à jour des données (20) | 2,076 | 99,590 |
| latence p99 | 75 à 235 ms (en file d'attente) | 8 à 16 ms |
La valeur par défaut utilise quatre processeurs de rendu (quatre cœurs) par rapport à celui du processeur sérialisé. Il s'agit donc d'une configuration et d'un gain de parallélisme, et non par cœur. Ce que le pool ajoute au-delà des cœurs supplémentaires : il supprime la sérialisation unique en vol (un travailleur sérialisé est inactif entre les allers-retours IPC) et, avec le chemin d'évaluation asynchrone, le désabonnement du planificateur par demande - ainsi le débit s'adapte à avec des travailleurs là où un pool de blocage naïf régresse en dessous d'un travailleur. Les mises à jour de données augmentent le plus (48 ×) car les écritures sérialisées deviennent simultanées.
contre Next.js, Remix, Astro, Qwik#
Par défaut performant, chaque framework comme un seul processus serveur (les applications Node s'exécutent en monothread - leur node server.js par défaut) :
| Cadre | JSON | Texte brut | Requête unique | Requête multiple (20) | Fortunes | Mises à jour (20) |
|---|---|---|---|---|---|---|
| bext (par défaut performant) | 91,047 | 96,579 | 89,613 | 81,407 | 91,320 | 90,803 |
| bext (1 travailleur sérialisé) | 6,419 | 7,060 | 5,948 | 4,023 | 4,519 | 2,076 |
| Astro | 5,512 | 5,214 | 4,791 | 4,118 | 5,027 | 796 |
| Qwik | 4,788 | 5,038 | 4,696 | 3,284 | 4,400 | 766 |
| Remixer | 3,787 | 4,240 | 3,875 | 2,950 | 4,069 | 791 |
| Next.js | 2,945 | 3,245 | 3,149 | 2,623 | 3,081 | 711 |
La ligne** du travailleur sérialisé **1 est la même configuration conservatrice que la colonne de gauche du tableau ci-dessus : un rendu en vol sur un travailleur, pas de multiplexage. Même là, bext atteint ou dépasse le débit de processus unique des autres frameworks sur la plupart des tests (et est loin devant sur les mises à jour), tout en effectuant un rendu V8 complet par requête ; un travailleur sérialisé est limité à la latence, ses numéros sont donc stables quelle que soit la charge de la boîte.
p99 au pic : bext (par défaut performant) 10 à 17 ms ; les autres 18 à 82 ms. Un seul serveur bext utilise la machine entière – son maître Rust évente les requêtes sur un pool de rendu V8 multiplexé – où un seul processus node exécute une boucle d'événements et a besoin d'un gestionnaire de cluster externe pour utiliser plus de cœurs. Donnez aux applications Node cluster avec N Workers et leur nombre augmente d'environ N × ; la comparaison par serveur ci-dessus reflète le déploiement à processus unique par défaut de chaque framework.
Rendu et mise en cache des pages PRISM#
Il s’agit d’une mesure distincte au niveau de la demande capturée le 05/07/2026. Il utilise une page, une configuration de travail et un générateur de charge HTTP différents de l'actuel Fusillade PRISM de 38,3 Ko de rendu uniquement ; ne dérivez pas les ratios de moteur par rendu en combinant les deux tableaux.
Les tests ci-dessus sont des gestionnaires minimaux. Une vraie page PRISM — la « Vitrine des widgets » : une arborescence de composants rendue par le serveur avec trois îlots pouvant être repris et une grille de produits — est un rendu beaucoup plus lourd. La même page, mesurée à travers trois états de cache :
| Configuration | rps | Qu'est-ce qui court |
|---|---|---|
| 1 travailleur sérialisé | 1,450 | rendu V8 complet, un en vol |
Par défaut performant ( -w4 ) |
30,174 | rendu V8 complet, pool multiplexé |
| Cache ISR atteint | ~456,000 | HTML stocké servi depuis la mémoire, pas de rendu |
Le rendu non mis en cache exécute l'intégralité de l'arborescence des composants dans la V8 à chaque requête (une page réelle est ~ 20 fois plus lourde que les gestionnaires de requêtes/JSON triviaux, d'où les nombres inférieurs). L'accès au cache ISR sert le HTML précédemment rendu directement à partir de la mémoire - pas de V8, pas de chargeur - un saut d'environ 15 fois par rapport au rendu en direct et environ 5 fois le gestionnaire non mis en cache le plus rapide. Les sites réels mélangent ces éléments : les routes force-dynamic sont restituées à chaque fois, les routes ISR/statiques sont utilisées à partir du cache et bext court-circuite en outre les rendus répétés via un cache de lecture par chargeur et un cache de rendu déterminisme par observation lorsque la sortie d'une route s'avère stable.
Qu'est-ce qui motive les chiffres#
- Aucun travail sur le système de fichiers par requête. Découverte d'itinéraire et compilation de chaque itinéraire les bundles sont mis en cache ; les appels système principaux sont passés de ~67 à ~15 par requête.
- Pool multiplexé par défaut (
BEXT_V8_POOL) : le thread de rendu ne s'arrête jamais lors de l'aller-retour IPC. - Évaluation asynchrone (
BEXT_V8_ASYNC_EVAL) : le travailleur HTTP attend le rendu au lieu de garer un thread bloquant, réduisant de moitié les changements de contexte par requête et permettant au pool d'évoluer. - SQLite réglé sur le chemin de la base de données de rendu en cours (WAL,
synchronous=NORMAL, mmap) plus une primitive de requête par lots — un croisement V8 pour N lectures, une transaction pour N écritures.