perf(grid): optimize state store with O(1) row indexing and debounced summary calculations

This commit is contained in:
Michael committed 2026-05-26 13:53:51 +02:00
1 parent 8f2c4c77ba
commit d75102fb18
6 files changed
+86 -37

No files matched your search

@@ -5,7 +5,7 @@ import { useSession } from "next-auth/react";
import { BulkAiAnalyzer } from "./bulk-ai-analyzer";
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera, ChevronDown } from "lucide-react";
import { useState, useTransition } from "react";
import { useMemo, useState, useTransition } from "react";
import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
import { SuccessModal } from "@/components/shared/success-modal";
import {
@@ -45,7 +45,7 @@ export function FloatingSummaryBar() {
message: ""
});
const visibleCodeins = rows.map(r => r.codein);
const visibleCodeins = useMemo(() => rows.map(r => r.codein), [rows]);
const { save, hasDrafts, count } = useSaveDrafts(filters.magasin || "TOTAL", visibleCodeins);
const handleSave = () => {
@@ -15,11 +15,13 @@ interface GridFilterBarProps {
magasins: { code: string; nom: string }[];
}
const EMPTY_DRAFT_CHANGES: Record<string, string> = {};
export function GridFilterBar({ fournisseurs, magasins }: GridFilterBarProps) {
const filters = useGridStore((s) => s.filters);
const setFilter = useGridStore((s) => s.setFilter);
const rows = useGridStore((s) => s.rows);
const draftChanges = useGridStore((s) => s.draftChanges);
const draftChanges = useGridStore((s) => s.filters.codeGamme ? s.draftChanges : EMPTY_DRAFT_CHANGES);
const router = useRouter();
const searchParams = useSearchParams();
const [isRefreshing, setIsRefreshing] = React.useState(false);
@@ -86,6 +86,8 @@ interface HeatmapGridProps {
isAdmin?: boolean;
}
const EMPTY_DRAFT_CHANGES: Record<string, GammeCode> = {};
// =========================================================================
// OPTIMISATION PERFORMANCES (React.memo + Zustand Selectors granulaires)
// =========================================================================
@@ -272,7 +274,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
const rows = useGridStore((s) => s.rows);
const filters = useGridStore((s) => s.filters);
const displayDensity = useGridStore((s) => s.displayDensity);
const draftChanges = useGridStore((s) => s.draftChanges);
const draftChanges = useGridStore((s) => s.filters.codeGamme ? s.draftChanges : EMPTY_DRAFT_CHANGES);
const activeMagasin = useGridStore((s) => s.activeMagasin);
// Filtre client-side par code3 (famille) et codeGamme
@@ -707,7 +709,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
count: tableRows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: () => rowHeight,
overscan: 20,
overscan: 8,
});
const visibleColumns = table.getVisibleLeafColumns();
@@ -769,7 +771,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
<div
ref={tableContainerRef}
className="h-full w-full overflow-auto rounded-[12px] scroll-smooth relative"
className="h-full w-full overflow-auto rounded-[12px] relative"
style={{
background: "var(--bg-surface)",
border: "1px solid var(--border)",
@@ -835,7 +837,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
const isSelected = row.getIsSelected();
return (
<div key={row.id} ref={rowVirtualizer.measureElement} style={{ position: 'absolute', top: 0, left: 0, width: '100%' }}>
<div key={row.id} style={{ position: 'absolute', top: 0, left: 0, width: '100%' }}>
<GridRow
virtualRow={virtualRow}
row={row}
+11 -4
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { useGridStore } from "@/features/grid/store/use-grid-store";
import { saveDraftChanges } from "@/features/grid/api/save-draft-changes";
import { GammeCode } from "@/types/grid";
@@ -17,9 +17,16 @@ export function useSaveDrafts(magasin: string, filterCodeins?: string[]) {
const applyDraftsToRows = useGridStore((s) => s.applyDraftsToRows);
// Only consider changes that are in the filter list (if provided)
const activeDrafts = filterCodeins
? Object.fromEntries(Object.entries(draftChanges).filter(([codein]) => filterCodeins.includes(codein)))
: draftChanges;
const filterCodeinSet = useMemo(
() => filterCodeins ? new Set(filterCodeins) : null,
[filterCodeins]
);
const activeDrafts = useMemo(
() => filterCodeinSet
? Object.fromEntries(Object.entries(draftChanges).filter(([codein]) => filterCodeinSet.has(codein)))
: draftChanges,
[draftChanges, filterCodeinSet]
);
const count = Object.keys(activeDrafts).length;
+52 -12
View File
@@ -7,6 +7,8 @@ import type { GammeCode, GridFilters, GridSummary, ProductRow } from "@/types/gr
interface GridState {
/** Source data from server */
rows: ProductRow[];
/** Fast lookup by codein; not persisted */
rowsByCodein: Record<string, ProductRow>;
/** Draft edits: codein → new GammeCode */
draftChanges: Record<string, GammeCode>;
filters: GridFilters;
@@ -40,6 +42,14 @@ interface GridState {
setColumnSizing: (updater: Record<string, number> | ((old: Record<string, number>) => Record<string, number>)) => void;
}
let summaryTimer: ReturnType<typeof setTimeout> | null = null;
function indexRows(rows: ProductRow[]): Record<string, ProductRow> {
const indexed: Record<string, ProductRow> = {};
for (const row of rows) indexed[row.codein] = row;
return indexed;
}
function computeSummary(rows: ProductRow[], drafts: Record<string, GammeCode>, magasin = "TOTAL"): GridSummary {
const active = rows.filter((r) => {
const g = drafts[r.codein] ?? r.codeGamme;
@@ -64,6 +74,7 @@ export const useGridStore = create<GridState>()(
persist(
(set, get) => ({
rows: [],
rowsByCodein: {},
activeMagasin: "TOTAL",
draftChanges: {},
filters: {
@@ -89,17 +100,26 @@ export const useGridStore = create<GridState>()(
columnSizing: {},
setActiveMagasin: (code) => {
const { rows, draftChanges } = get();
set({ activeMagasin: code, summary: computeSummary(rows, draftChanges, code) });
set({ activeMagasin: code });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
setRows: (rows) => {
set({ rows, summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
set({ rows, rowsByCodein: indexRows(rows) });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 120);
},
setDraftGamme: (codein, gamme) => {
const { rows, draftChanges: oldDrafts } = get();
const originalRow = rows.find(r => r.codein === codein);
const { rowsByCodein, draftChanges: oldDrafts } = get();
const originalRow = rowsByCodein[codein];
const originalGamme = originalRow?.codeGamme;
const draftChanges = { ...oldDrafts };
@@ -112,16 +132,31 @@ export const useGridStore = create<GridState>()(
draftChanges[codein] = gamme;
}
set({ draftChanges, summary: computeSummary(rows, draftChanges, get().activeMagasin) });
set({ draftChanges });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
resetDrafts: () => {
set({ draftChanges: {}, summary: computeSummary(get().rows, {}, get().activeMagasin) });
set({ draftChanges: {} });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, {}, state.activeMagasin) });
}, 80);
},
clearDrafts: (codeins) => {
const draftChanges = { ...get().draftChanges };
codeins.forEach((id) => delete draftChanges[id]);
set({ draftChanges, summary: computeSummary(get().rows, draftChanges, get().activeMagasin) });
set({ draftChanges });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(state.rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
/** Apply saved drafts onto row.codeGamme so isModified works after clearing drafts */
applyDraftsToRows: (draftsToApply: Record<string, GammeCode>) => {
@@ -132,7 +167,7 @@ export const useGridStore = create<GridState>()(
}
return r;
});
set({ rows });
set({ rows, rowsByCodein: indexRows(rows), summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
},
setFilter: (key, value) => {
set((state) => ({ ...state, filters: { ...state.filters, [key]: value } }));
@@ -144,11 +179,11 @@ export const useGridStore = create<GridState>()(
},
batchSetDraftGamme: (newChanges) => {
const { rows, draftChanges: oldDrafts } = get();
const { rows, rowsByCodein, draftChanges: oldDrafts } = get();
const updatedDrafts = { ...oldDrafts };
Object.entries(newChanges).forEach(([codein, gamme]) => {
const originalRow = rows.find(r => r.codein === codein);
const originalRow = rowsByCodein[codein];
const originalGamme = originalRow?.codeGamme;
if (gamme === originalGamme) {
@@ -158,7 +193,12 @@ export const useGridStore = create<GridState>()(
}
});
set({ draftChanges: updatedDrafts, summary: computeSummary(rows, updatedDrafts, get().activeMagasin) });
set({ draftChanges: updatedDrafts });
if (summaryTimer) clearTimeout(summaryTimer);
summaryTimer = setTimeout(() => {
const state = get();
set({ summary: computeSummary(rows, state.draftChanges, state.activeMagasin) });
}, 80);
},
setColumnVisibility: (updater) => {
+12 -14
View File
@@ -1,22 +1,20 @@
# Plan - Commit et Push des modifications locales
# Plan - Commit et Push des optimisations de performances de la grille
## Contexte
L'utilisateur a demandé de commiter et pusher ses modifications locales actuelles. L'analyse montre qu'il s'agit d'une implémentation majeure du chargement en streaming pour la grille de produits (11 fichiers modifiés et 2 fichiers non suivis).
L'utilisateur a demandé de commiter et pusher de nouvelles modifications locales. L'analyse révèle des optimisations de performances clés apportées au store Zustand de la grille :
1. Indexation rapide des lignes de produits par `codein` dans `rowsByCodein` pour accélérer la mise à jour des brouillons de O(N) à O(1).
2. Debouncing du calcul des résumés (`computeSummary`) afin d'alléger le thread principal lors des imports en streaming ou des modifications de masse.
## Focus Actuel
Validation finale et dépôt propre.
Validation de la compilation, staging des optimisations de performance, commit et push.
## Master Plan
- [x] Analyser l'état actuel de Git et identifier les fichiers modifiés/ajoutés (Fait)
- [x] Vérifier la compilation globale avec `rtk tsc` pour s'assurer que tout est propre (Fait)
- [x] Ajouter les fichiers à l'index Git (`rtk git add`) (Fait)
- [x] Créer le commit avec un message descriptif et professionnel (`rtk git commit`) (Fait: "feat(grid): implement product streaming with JSON chunks for grid client")
- [x] Envoyer les modifications sur la branche distante (`rtk git push`) (Fait)
- [x] Valider le statut Git final (Fait)
- [x] Analyser l'état de Git et identifier les nouveaux fichiers modifiés (Fait: 5 fichiers)
- [ ] Vérifier la compilation globale du projet avec `rtk tsc`
- [ ] Ajouter les fichiers à l'index Git (`rtk git add`)
- [ ] Créer le commit avec un message décrivant les optimisations de performance (`rtk git commit`)
- [ ] Envoyer le commit vers le dépôt distant (`rtk git push`)
- [ ] Valider le statut Git final
## Progress Log
- **2026-05-26 13:31** : Analyse initiale des modifications. Identification de l'implémentation de la grille en streaming (`src/features/grid` et `/api/grid`). Initialisation du plan de commit et push.
- **2026-05-26 13:32** : Vérification de la compilation effectuée via `rtk tsc`. Aucun problème introduit dans les fichiers de la grille.
- **2026-05-26 13:33** : Ajout de tous les fichiers modifiés et nouveaux à l'index Git via `rtk git add .`.
- **2026-05-26 13:34** : Création du commit avec le message descriptif sur le streaming de la grille.
- **2026-05-26 13:35** : Push réussi des modifications vers la branche distante `origin/main`.
- **2026-05-26 13:53** : Analyse des nouvelles modifications. Découverte des optimisations majeures sur `use-grid-store.ts` (indexation O(1) et debouncing des calculs de résumé).