Ordre matriciel : ingénierie de grilles B2B hautes performances
Une plongée approfondie dans la création de grilles de commande matricielles B2B hautes performances pour des milliers de variantes. Maîtrise de la virtualisation React, de la gestion des états et du traitement par lots d'API.
Dans le monde trépidant du commerce électronique B2C, le parcours utilisateur est linéaire : parcourir, sélectionner, ajouter au panier. Mais le B2B est une tout autre bête. Un acheteur en gros pour un détaillant de mode ne veut pas cliquer cinquante fois sur « Ajouter au panier » pour cinquante tailles de chemises différentes. Ils veulent de l’efficacité. Ils veulent de la vitesse. Ils veulent une Matrice.
Chez Maison Code Paris, nous avons vu des portails B2B s’effondrer sous leur propre poids. Nous avons vu des solutions « d’entreprise » qui prennent 4 secondes pour afficher une page produit car elles restituent naïvement 2 000 entrées pour un seul type de vis. C’est inacceptable. Dans l’économie de gros, les frictions ne gênent pas seulement l’utilisateur ; cela détruit la boucle de revenus récurrents.
Ce guide est une plongée technique approfondie dans la création de « Excel du commerce électronique » : la grille de commande matricielle.
Pourquoi Maison Code en parle
Chez Maison Code Paris, nous agissons comme la conscience architecturale de nos clients. Nous héritons souvent de stacks “modernes” construites sans compréhension fondamentale de l’échelle.
Nous abordons ce sujet car il représente un point de pivot critique dans la maturité de l’ingénierie. Une mise en œuvre correcte différencie un MVP fragile d’une plateforme résiliente de niveau entreprise.
Pourquoi Maison Code discute de la commande matricielle
Nous construisons de vastes plateformes B2B pour les maisons de luxe et les géants industriels. La différence entre un outil qui ressemble à un SaaS moderne et un outil qui ressemble à un ERP existant est souvent Matrix Grid. Lorsqu’un acheteur peut saisir des quantités pour 500 variantes en quelques secondes, à l’aide de la navigation au clavier, sans décalage, il se sent professionnel. Ils se sentent respectés.
Nous en discutons car il se situe à l’intersection de contraintes techniques lourdes (limites DOM, limites API) et d’une expérience utilisateur de grande valeur. Le résoudre nécessite plus qu’une simple bibliothèque d’interface utilisateur ; cela nécessite une compréhension fondamentale de la manière dont le navigateur s’affiche et de la manière dont l’état circule dans une application.
Le problème d’évolutivité : le rendu O(n)
Prenons un simple produit B2B : un T-shirt.
- Couleurs : 10
- Tailles : 8
- Total des variantes : 80 entrées.
React vous permet de restituer 80 entrées sans transpirer.
Considérons maintenant un boulon industriel.
- Longueurs : 50
- Pas de filetage : 20
- Matériaux : 10
- Total des variantes : 10 000 entrées.
Si vous tentez de restituer simultanément 10 000 éléments <input> dans le DOM, votre application se bloquera. Le goulot d’étranglement n’est pas l’exécution de JavaScript ; ce sont les phases Layout et Paint du moteur du navigateur. Chaque fois que l’utilisateur tape un caractère dans une case, si votre gestion d’état est naïve, React peut tenter de réconcilier l’ensemble de l’arborescence.
Le coût du nouveau rendu
Si vous utilisez un seul objet useState pour l’ensemble du formulaire :
const [formState, setFormState] = useState({});
Chaque frappe déclenche un nouveau rendu du composant parent, qui restitue ensuite 10 000 enfants. Même avec React.memo, la différence entre les accessoires à elle seule pour 10 000 composants entraînera un décalage d’entrée notable. Dans un outil professionnel, l’input lag est fatal.
Phase 1 : Virtualisation (La solution DOM)
La première étape vers la performance consiste à reconnaître que l’utilisateur ne peut pas consulter 10 000 entrées à la fois. Un moniteur typique affiche peut-être 50 lignes de données.
La Virtualisation (ou « Fenêtrage ») est la technique permettant de restituer uniquement les nœuds DOM actuellement visibles dans la fenêtre. Au fur et à mesure que l’utilisateur fait défiler, nous détruisons les nœuds quittant le haut de l’écran et en créons de nouveaux entrant par le bas. Le navigateur pense qu’il fait défiler un élément de 5 000 px, mais le DOM ne contient que 50 « div ».
Nous recommandons TanStack Virtual (sans tête) ou react-window pour cette implémentation.
Modèle de mise en œuvre
Voici comment nous structurons une grille virtualisée pour une matrice B2B. Nous traitons la grille comme un système de coordonnées (Ligne, Colonne).
importer { useVirtualizer } depuis '@tanstack/react-virtual' ;
importer { useRef } depuis 'react' ;
// La "Source Unique de Vérité" pour les données matricielles
// Idéalement, généralement aplati ou structuré comme Map<VariantID, Quantity>
tapez MatrixData = Record<string, number> ;
export const VirtualizedMatrix = ({ lignes, colonnes, données } : MatrixProps) => {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
nombre : lignes.longueur,
getScrollElement : () => parentRef.current,
estimateSize : () => 50, // hauteur de ligne de 50 px
overscan : 5, // Rendu de 5 lignes supplémentaires pour un défilement fluide
});
retour (
<div
ref={parentRef}
style={{
hauteur : `600px`,
débordement : 'auto',
}}
>
<div
style={{
hauteur : `${rowVirtualizer.getTotalSize()}px`,
largeur : '100%',
position : 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const rowData = lignes[virtualRow.index];
retour (
<div
key={virtualRow.key}
style={{
position : 'absolue',
haut : 0,
gauche : 0,
largeur : '100%',
hauteur : `${virtualRow.size}px`,
transformer : `translateY(${virtualRow.start}px)`,
affichage : 'flex',
}}
>
{/* Afficher les colonnes (cellules) ici */}
<RowLabel label={rowData.name} />
{colonnes.map((col) => (
<EntréeMatrice
clé={col.id}
variantId={getVariantId(rowData, col)}
/>
))}
</div>
);
})}
</div>
</div>
);
} ;
À retenir : La virtualisation réduit le temps de chargement initial de 3,5 s à 0,2 s pour les grands ensembles de données. Il est non négociable pour les catalogues dépassant 500 variantes.
Phase 2 : Gestion de l’état (la solution mémoire)
La virtualisation résout le problème du rendu, mais nous avons toujours un problème d’état. Si nous conservons l’état de 10 000 entrées dans React State, la mise à jour d’une nécessite une optimisation minutieuse pour éviter de déclencher une mise à jour à l’échelle de l’arborescence.
L’approche signal
Chez Maison Code, nous privilégions les Signaux (via @preact/signals-react ou des abonnés purement granulaires) ou les Composants non contrôlés pour les grilles matricielles.
Si nous utilisons des composants non contrôlés, nous contournons entièrement le cycle de rendu de React pour l’action de frappe.
- Lire :
defaultValue={data.get(id)} - Écrire :
onChange={(e) => data.set(id, e.target.value)}(Mutation directe ou mise à jour de la référence) - Soumettre : lire à partir de la référence/carte.
Cependant, nous avons souvent besoin de État calculé (par exemple, « Quantité totale : 150 »). Cela nous replace dans le domaine de la réactivité.
La meilleure approche moderne est Zustand with Transient Updates.
// magasin.ts
importer { créer } depuis 'zustand' ;
interface MatrixStore {
quantités : Record<string, number> ;
setQuantity : (id : chaîne, quantité : nombre) => void ;
// Valeurs calculées dérivées des composants ou des sélecteurs au sein des composants
}
export const useMatrixStore = create<MatrixStore>((set) => ({
quantités : {},
setQuantity : (id, quantité) => set((state) => ({
quantités : { ...état.quantités, [id] : quantité }
})),
}));
// MatrixInput.tsx
// Ce composant s'abonne UNIQUEMENT à sa tranche d'état spécifique
const MatrixInput = ({varianteId}) => {
const qty = useMatrixStore((state) => state.quantities[variantId] || 0);
const setQuantity = useMatrixStore((state) => state.setQuantity);
retour (
<entrée
valeur={qté}
onChange={(e) => setQuantity(variantId, parseInt(e.target.value))}
/>
);
}
Cela garantit que la saisie dans une cellule ne restitue que cette cellule spécifique (et peut-être le compteur « Total »), plutôt que la grille entière.
Phase 3 : Micro-interactions & UX
La vitesse est technique, mais elle est aussi perceptuelle. Un acheteur B2B s’attend à ce que l’outil se comporte comme une feuille de calcul.
Navigation au clavier
Une interface gourmande en souris est trop lente pour une saisie groupée. Nous devons implémenter Navigation par touches fléchées.
- Entrez : Descendre (standard de feuille de calcul).
- Onglet : Déplacer vers la droite.
- Touches fléchées : Déplacez-vous dans les directions respectives.
Cela nécessite de gérer le focus par programmation. Puisque nous virtualisons, l’entrée sur laquelle vous souhaitez vous concentrer peut ne pas encore exister dans le DOM. C’est la partie la plus délicate. Vous devez faire défiler le virtualiseur jusqu’à l’index avant d’essayer de vous concentrer.
Commentaires instantanés sur l’inventaire
Les utilisateurs ne doivent pas attendre « Ajouter au panier » pour savoir qu’une variante est en rupture de stock. Nous pré-récupérons les données d’inventaire dans un format léger :
{
"variante_1": 50,
"variante_2": 0,
"variante_3": 1200
}
Nous mappons cela à un état visuel.
- Gris : En rupture de stock (0).
- Avertissement jaune : stock faible (l’utilisateur a tapé 50, le stock est de 40).
- Bordure rouge : entrée invalide.
Cette validation doit avoir lieu de manière synchrone côté client.
Phase 4 : Charge utile et traitement par lots de l’API
L’utilisateur clique sur “Ajouter à la commande”. Ils ont sélectionné 150 variantes uniques. La plupart des API REST (y compris l’API Cart standard de Shopify) ne sont pas conçues pour ingérer efficacement 150 éléments de campagne dans une seule requête HTTP POST. Il peut expirer ou dépasser les limites de charge utile.
Stratégie : la file d’attente des promesses groupées
Nous ne bloquons jamais l’interface utilisateur. Nous affichons une barre de progression (“Ajout d’éléments… 40%”).
const BATCH_SIZE = 50 ;
fonction asynchrone addToCartRecursive(items: Item[]) {
if (items.length === 0) return ;
const chunk = items.slice(0, BATCH_SIZE);
const restant = items.slice(BATCH_SIZE);
// Mise à jour optimiste de l'interface utilisateur ici
essayez {
attendre api.cart.add(morceau);
updateProgress((total - restant.length) / total);
return addToCartRecursive (restant); // Lot suivant
} attraper (erreur) {
handlePartialFailure (morceau, erreur);
}
}
Stratégie : la transformation du panier (Shopify)
Pour les marchands Shopify Plus, nous utilisons les Cart Transform Functions ou Bundles. Nous pouvons ajouter un article parent unique (« The Matrix Bundle ») au panier et laisser la logique backend l’étendre en éléments de campagne lors du paiement. Cela permet aux interactions avec le panier d’être extrêmement rapides tout en préservant la logique backend pour l’exécution. Consultez notre guide sur Checkout Extensibility pour en savoir plus à ce sujet.
Mobile : le pivot
Une grille 50x20 est impossible sur mobile. N’essayez pas de le rendre réactif en réduisant les cellules. C’est inutilisable.
Sur mobile, nous Pivotons l’interface utilisateur. Au lieu de « Rows = Sizes » et « Cols = Colors », nous affichons simplement l’attribut principal (par exemple, la couleur) sous forme de liste.
- L’utilisateur appuie sur “Rouge”.
- Un accordéon se dilate (ou une feuille inférieure s’ouvre).
- L’utilisateur voit une liste de tailles pour “Rouge”.
- L’utilisateur saisit les quantités.
- L’utilisateur réduit “Rouge” et appuie sur “Bleu”.
Cette approche « Drill-down » respecte l’espace du petit écran tout en gardant intacte la hiérarchie des données.
## Benchmarks de performances
Lorsque nous avons migré un client majeur d’un formulaire React standard vers une matrice Zustand virtualisée, nous avons observé :
| Métrique | Grille héritée (Standard React) | Matrice de code Maison (Virtualisée) | Amélioration | | :