Ordinamento a matrice: progettazione di griglie B2B ad alte prestazioni
Un approfondimento sulla creazione di griglie di ordinazione a matrice B2B ad alte prestazioni per migliaia di varianti. Padronanza della virtualizzazione di React, della gestione dello stato e del batching delle API.
Nel frenetico mondo dell’e-commerce B2C, il percorso dell’utente è lineare: Sfoglia, Seleziona, Aggiungi al carrello. Ma il B2B è una bestia completamente diversa. Un acquirente all’ingrosso per un rivenditore di moda non vuole fare clic su “Aggiungi al carrello” cinquanta volte per cinquanta taglie di magliette diverse. Vogliono l’efficienza. Vogliono la velocità. Vogliono un Matrix.
A Maison Code Paris abbiamo visto i portali B2B sgretolarsi sotto il loro stesso peso. Abbiamo visto soluzioni “enterprise” che impiegano 4 secondi per eseguire il rendering di una pagina di prodotto perché eseguono ingenuamente il rendering di 2.000 input per un singolo tipo di vite. Questo è inaccettabile. Nell’economia all’ingrosso, l’attrito non infastidisce solo l’utente; distrugge il ciclo ricorrente delle entrate.
Questa guida è un’analisi approfondita della progettazione dell‘“Excel dell’e-commerce”: la griglia di ordinazione a matrice.
Perché Maison Code ne parla
In Maison Code Paris, agiamo come la coscienza architettonica dei nostri clienti. Spesso ereditiamo stack “moderni” costruiti senza una comprensione fondamentale della scala.
Discutiamo di questo argomento perché rappresenta un punto di svolta critico nella maturità ingegneristica. Implementarlo correttamente differenzia un MVP fragile da una piattaforma resiliente di livello aziendale.
Perché Maison Code parla dell’ordinamento a matrice
Costruiamo ampie piattaforme B2B per case di lusso e giganti industriali. La differenza tra uno strumento che sembra un moderno SaaS e uno che sembra un ERP legacy è spesso la Matrix Grid. Quando un acquirente può inserire quantità per 500 varianti in pochi secondi, utilizzando la navigazione da tastiera, senza ritardi, si sente professionale. Si sentono rispettati.
Ne discutiamo perché si trova all’intersezione tra pesanti vincoli tecnici (limiti DOM, limiti API) e esperienza utente di alto valore. Risolverlo richiede più di una semplice libreria dell’interfaccia utente; richiede una comprensione fondamentale di come viene eseguito il rendering del browser e di come lo stato scorre attraverso un’applicazione.
Il problema della scalabilità: rendering O(n).
Considera un semplice prodotto B2B: una maglietta.
- Colori: 10
- Taglie: 8
- Varianti totali: 80 ingressi.
React ti consente di eseguire il rendering di 80 input senza sudare.
Consideriamo ora un bullone industriale.
- Lunghezze: 50
- Passi della filettatura: 20
- Materiali: 10
- Varianti totali: 10.000 input.
Se provi a eseguire il rendering simultaneo di 10.000 elementi <input> nel DOM, la tua applicazione si bloccherà. Il collo di bottiglia non è l’esecuzione di JavaScript; sono le fasi Layout e Paint del motore del browser. Ogni volta che l’utente digita un carattere in una casella, se la gestione dello stato è ingenua, React potrebbe tentare di riconciliare l’intero albero.
Il costo del nuovo rendering
Se utilizzi un singolo oggetto useState per l’intero modulo:
“tsx const [formState, setFormState] = useState({});
Ogni pressione di un tasto attiva un nuovo rendering del componente principale, che quindi esegue nuovamente il rendering di 10.000 bambini. Anche con "React.memo", la sola differenza dell'elica per 10.000 componenti causerà un notevole ritardo di input. In uno strumento professionale, l'input lag è fatale.
## Fase 1: virtualizzazione (la soluzione DOM)
Il primo passo verso le prestazioni è riconoscere che l'utente non può guardare 10.000 input contemporaneamente. Un monitor tipico visualizza forse 50 righe di dati.
La **virtualizzazione** (o "Windowing") è la tecnica di rendering *solo* dei nodi DOM attualmente visibili nel viewport. Mentre l'utente scorre, distruggiamo i nodi che lasciano la parte superiore dello schermo e ne creiamo di nuovi che entrano nella parte inferiore. Il browser pensa che stia scorrendo un elemento di 5.000 px, ma il DOM contiene solo 50 `div`.
Consigliamo **TanStack Virtual** (headless) o **react-window** per questa implementazione.
### Modello di implementazione
Ecco come strutturiamo una griglia virtualizzata per una matrice B2B. Trattiamo la griglia come un sistema di coordinate (riga, colonna).
"tsx
import { useVirtualizer } da '@tanstack/react-virtual';
importa { useRef } da 'react';
// La "fonte unica della verità" per i dati della matrice
// Idealmente in genere appiattito o strutturato come Map<VariantID, Quantità>
digitare MatrixData = Record<stringa, numero>;
esporta const VirtualizedMatrix = ({righe, colonne, dati}: MatrixProps) => {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
conteggio: righe.lunghezza,
getScrollElement: () => parentRef.current,
stimaSize: () => 50, // Altezza riga 50px
overscan: 5, // Renderizza 5 righe extra per uno scorrimento fluido
});
ritorno (
<div
riferimento={Rifgenitore}
stile={{
altezza: `600px`,
overflow: 'auto',
}}
>
<div
stile={{
altezza: `${rowVirtualizer.getTotalSize()}px`,
larghezza: '100%',
posizione: 'relativa',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const rowData = righe[virtualRow.index];
ritorno (
<div
chiave={rigavirtuale.chiave}
stile={{
posizione: 'assoluta',
superiore: 0,
sinistra: 0,
larghezza: '100%',
altezza: `${virtualRow.size}px`,
trasformazione: `translateY(${virtualRow.start}px)`,
visualizzazione: 'flessibile',
}}
>
{/*Esegui il rendering delle colonne (celle) qui */}
<etichetta RowLabel={rowData.name} />
{colonne.mappa((col) => (
<MatriceInput
chiave={col.id}
VariantId={getVariantId(rowData, col)}
/>
))}
</div>
);
})}
</div>
</div>
);
};
Punto chiave: la virtualizzazione riduce il tempo di caricamento iniziale da 3,5 s a 0,2 s per set di dati di grandi dimensioni. Non è negoziabile per cataloghi che superano le 500 varianti.
Fase 2: Gestione dello stato (la soluzione della memoria)
La virtualizzazione risolve il problema di rendering, ma abbiamo ancora un problema di stato. Se manteniamo lo stato di 10.000 input in React State, aggiornarne uno richiede un’attenta ottimizzazione per evitare di attivare un aggiornamento a livello di albero.
L’approccio del segnale
Noi di Maison Code preferiamo Segnali (tramite @preact/signals-react o abbonati puramente granulari) o Componenti non controllati per le griglie a matrice.
Se utilizziamo Componenti non controllati, ignoriamo completamente il ciclo di rendering di React per l’azione di digitazione.
- Leggi:
defaultValue={data.get(id)} - Scrivi:
onChange={(e) => data.set(id, e.target.value)}(Mutazione diretta o aggiornamento Ref) - Invia: leggi dal riferimento/mappa.
Tuttavia, spesso abbiamo bisogno di Stato calcolato (ad esempio, “Quantità totale: 150”). Questo ci riporta nel regno della reattività.
Il miglior approccio moderno è Zustand con aggiornamenti temporanei.
“tsx // negozio.ts importa { crea } da ‘zustand’;
interfaccia MatrixStore { quantità: Record<stringa, numero>; setQuantity: (id: stringa, qtà: numero) => void; // Valori calcolati derivati in componenti o selettori all’interno dei componenti }
export const useMatrixStore = create
// MatrixInput.tsx // Questo componente si iscrive SOLO alla sua specifica porzione di stato const MatrixInput = ({ VariantId }) => { const qty = useMatrixStore((stato) => state.quantities[variantId] || 0); const setQuantity = useMatrixStore((state) => state.setQuantity);
ritorno (
<ingresso
valore={qtà}
onChange={(e) => setQuantity(variantId, parseInt(e.target.value))}
/>
);
}
Ciò garantisce che digitando in una cella venga nuovamente visualizzato *quella cella specifica* (e forse il contatore "Totale"), anziché l'intera griglia.
## Fase 3: microinterazioni e UX
La velocità è tecnica, ma è anche percettiva. Un acquirente B2B si aspetta che lo strumento si comporti come un foglio di calcolo.
### Navigazione tramite tastiera
Un'interfaccia pesante per il mouse è troppo lenta per l'immissione in blocco. Dobbiamo implementare la **Navigazione con tasti freccia**.
* **Invio**: sposta verso il basso (standard del foglio di calcolo).
* **Tab**: sposta a destra.
* **Tasti freccia**: spostati nelle rispettive direzioni.
Ciò richiede la gestione del focus a livello di codice. Dato che stiamo virtualizzando, l'input su cui vuoi concentrarti *potrebbe non esistere ancora nel DOM*. Questa è la parte difficile. È necessario scorrere il virtualizzatore fino all'indice *prima* di tentare la messa a fuoco.
### Feedback istantaneo sull'inventario
Gli utenti non devono attendere "Aggiungi al carrello" per sapere che una variante è esaurita.
Prerecuperiamo i dati di inventario in un formato leggero:
```json
{
"variante_1": 50,
"variante_2": 0,
"variante_3": 1200
}
Mappiamo questo a uno stato visivo.
- In grigio: esaurito (0).
- Avviso giallo: scorte in esaurimento (l’utente ha digitato 50, le scorte sono 40).
- Bordo rosso: input non valido.
Questa convalida deve avvenire in modo sincrono sul lato client.
Fase 4: payload e batch API
L’utente fa clic su “Aggiungi all’ordine”. Hanno selezionato 150 varianti uniche. La maggior parte delle API REST (inclusa l’API Carrello standard di Shopify) non sono progettate per importare in modo efficiente 150 elementi pubblicitari in una singola richiesta HTTP POST. Potrebbe scadere o superare i limiti di carico utile.
Strategia: la coda delle promesse in batch
Non blocchiamo mai l’interfaccia utente. Mostriamo una barra di avanzamento (“Aggiunta elementi… 40%”).
“dattiloscritto”. const BATCH_SIZE = 50;
funzione asincrona addToCartRecursive(elementi: Articolo[]) { if (items.length === 0) return;
const pezzo = elementi.slice(0, BATCH_SIZE);
const rimanente = items.slice(BATCH_SIZE);
// Aggiornamento ottimistico dell'interfaccia utente qui
prova {
attendono api.cart.add(chunk);
updateProgress((totale - lunghezza.rimanente) / totale);
return addToCartRecursive(rimanente); // Lotto successivo
} cattura (errore) {
handlePartialFailure(pezzo, errore);
}
}
<hr style="margin: 1rem 0" />
### Strategia: la trasformazione del carrello (Shopify)
Per i commercianti Shopify Plus, utilizziamo le **Funzioni di trasformazione del carrello** o i **Bundle**. Possiamo aggiungere un *singolo* articolo principale ("The Matrix Bundle") al carrello e lasciare che la logica del backend lo espanda in elementi pubblicitari al momento del pagamento. Ciò mantiene le interazioni del carrello estremamente veloci preservando la logica di backend per l'adempimento.
Consulta la nostra guida su [Checkout Extensibility](/it/blog/tech-checkout-extensibility-it) per ulteriori informazioni al riguardo.
## Mobile: il perno
Una griglia 50x20 è impossibile sui dispositivi mobili. Non cercare di renderlo reattivo restringendo le celle. È inutilizzabile.
Sui dispositivi mobili, **ruotiamo l'interfaccia utente**.
Invece di `Righe = Dimensioni` e `Cols = Colori`, mostriamo semplicemente l'**Attributo primario** (ad esempio, Colore) come un elenco.
* L'utente tocca "Rosso".
* Si espande una fisarmonica (o si apre un lenzuolo inferiore).
* L'utente visualizza un elenco di dimensioni per "Rosso".
* Quantità immesse dall'utente.
* L'utente comprime "Rosso" e tocca "Blu".
Questo approccio "Drill-down" rispetta lo spazio del piccolo schermo mantenendo intatta la gerarchia dei dati.
## Benchmark delle prestazioni
Quando abbiamo migrato un cliente importante da un modulo React standard a una matrice Zustand virtualizzata, abbiamo osservato:
| Metrico | Griglia legacy (reazione standard) | Maison Code Matrix (virtualizzato) | Miglioramento |
| :
**[Assumi i nostri architetti](/contact)**.