Performance optimizations: lazy rendering, row memoization, and data processing loops

This commit is contained in:
Christian Vidal Wolf
2026-01-27 21:43:25 +01:00
parent f521dd2921
commit 6b4bef66ec
4 changed files with 284 additions and 194 deletions
+120 -77
View File
@@ -56,6 +56,95 @@ const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'bl
);
};
const WeeklyRow: React.FC<{
row: WeeklyPivotRow;
weeks: string[];
onDrillDown?: (sku: string) => void;
stockMap?: Map<string, number>;
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
top50Mode: 'eu' | 'uk';
sortConfig: SortConfig;
renderGrowth: (current: number, previous: number) => React.ReactNode;
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth }) => {
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
const asin = row.asin.trim().toUpperCase();
if (top50Ranking) {
if (top50Mode === 'eu') {
const rank = top50Ranking.eu.get(asin);
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' });
} else {
const rank = top50Ranking.uk.get(asin);
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' });
}
}
return (
<tr className="hover:bg-white/[0.02] transition-colors group">
<td className="p-3 py-2 sticky left-0 z-10 bg-slate-900 group-hover:bg-slate-800 border-r border-white/10">
<div className="flex flex-col">
<div className="flex items-center gap-2 mb-0.5">
{ranks.map((r, i) => (
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
))}
<span
onClick={() => onDrillDown?.(row.sku)}
className={`text-xs font-black uppercase tracking-tighter truncate max-w-[120px] transition-all
${onDrillDown ? 'text-indigo-400 cursor-pointer hover:text-indigo-300 hover:underline' : 'text-indigo-400/70'}`}
title={onDrillDown ? `Click to see Ads detail for ${row.sku}` : ''}
>
{row.sku || '-'}
</span>
<span className="text-[10px] font-bold text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded border border-white/5">{row.asin}</span>
</div>
<div className="flex items-start gap-2 mb-1">
<span className="text-[11px] text-white/70 truncate w-[190px] leading-tight" title={row.title}>{row.title}</span>
{stockMap && (
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
)}
</div>
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
</div>
</td>
{weeks.map((week, idx) => {
const val = row.unitsByWeek[week] || 0;
const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0;
const spend = row.spendByWeek[week] || 0;
const prevSpend = row.spendByWeek[weeks[idx + 1]] || 0;
return (
<td key={week} className={`p-3 py-2 text-center border-r border-white/5 align-middle ${sortConfig?.key === week ? 'bg-white/[0.01]' : ''}`}>
<div className="flex flex-col items-center justify-center gap-0.5">
<div className="flex items-center gap-1">
<span className={`text-sm font-bold ${val > 0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
{val > 0 ? val.toLocaleString('de-DE') : '-'}
</span>
{val > 0 && renderGrowth(val, prevVal)}
</div>
<div className="flex flex-col items-center">
{spend > 0 && (
<div className="flex items-center gap-1">
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
{renderGrowth(spend, prevSpend)}
</div>
)}
{row.gvByWeek?.[week] > 0 && (
<div className="flex items-center gap-1">
<span className={`text-[9px] font-black tracking-tighter ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'text-teal-300' : 'text-teal-500/70'}`}>
GV: {row.gvByWeek[week].toLocaleString('de-DE')}
</span>
{renderGrowth(row.gvByWeek[week], row.gvByWeek?.[weeks[idx + 1]] || 0)}
</div>
)}
</div>
</div>
</td>
);
})}
</tr>
);
});
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange }) => {
// Pivot data - memoized
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
@@ -70,6 +159,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
const [growthThreshold, setGrowthThreshold] = useState(10);
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
const [displayCount, setDisplayCount] = useState(50);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Default sort: most recent week, descending, units
const [sortConfig, setSortConfig] = useState<SortConfig>(() => {
@@ -492,87 +583,39 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{paginatedRows.length > 0 ? (
paginatedRows.map((row) => {
const asin = row.asin.trim().toUpperCase();
const ranks = [];
if (top50Ranking) {
if (top50Mode === 'eu') {
const rank = top50Ranking.eu.get(asin);
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' as const });
} else {
const rank = top50Ranking.uk.get(asin);
if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' as const });
}
}
{sortedRows.length > 0 ? (
(() => {
const displayRows = sortedRows.slice(0, displayCount);
return (
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
<td className="p-3 py-2 sticky left-0 z-10 bg-slate-900 group-hover:bg-slate-800 border-r border-white/10">
<div className="flex flex-col">
<div className="flex items-center gap-2 mb-0.5">
{ranks.map((r, i) => (
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
))}
<span
onClick={() => onDrillDown?.(row.sku)}
className={`text-xs font-black uppercase tracking-tighter truncate max-w-[120px] transition-all
${onDrillDown ? 'text-indigo-400 cursor-pointer hover:text-indigo-300 hover:underline' : 'text-indigo-400/70'}`}
title={onDrillDown ? `Click to see Ads detail for ${row.sku}` : ''}
<>
{displayRows.map(row => (
<WeeklyRow
key={row.id}
row={row}
weeks={weeks}
onDrillDown={onDrillDown}
stockMap={stockMap}
top50Ranking={top50Ranking}
top50Mode={top50Mode}
sortConfig={sortConfig}
renderGrowth={renderGrowth}
/>
))}
{displayCount < sortedRows.length && (
<tr>
<td colSpan={weeks.length + 1} className="p-6 text-center bg-slate-900/50 backdrop-blur-sm border-t border-white/5">
<button
onClick={() => setDisplayCount(prev => prev + 100)}
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-black uppercase tracking-widest shadow-xl transition-all active:scale-95 border border-indigo-400/30"
>
{row.sku || '-'}
</span>
<span className="text-[10px] font-bold text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded border border-white/5">{row.asin}</span>
</div>
<div className="flex items-start gap-2 mb-1">
<span className="text-[11px] text-white/70 truncate w-[190px] leading-tight" title={row.title}>{row.title}</span>
{stockMap && (
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
)}
</div>
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
</div>
</td>
{weeks.map((week, idx) => {
const val = row.unitsByWeek[week] || 0;
const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0;
const spend = row.spendByWeek[week] || 0;
const prevSpend = row.spendByWeek[weeks[idx + 1]] || 0;
return (
<td key={week} className={`p-3 py-2 text-center border-r border-white/5 align-middle ${sortConfig?.key === week ? 'bg-white/[0.01]' : ''}`}>
<div className="flex flex-col items-center justify-center gap-0.5">
<div className="flex items-center gap-1">
<span className={`text-sm font-bold ${val > 0 ? (sortConfig?.key === week && sortConfig.metric === 'units' ? 'text-indigo-400' : 'text-white') : 'text-slate-700'}`}>
{val > 0 ? val.toLocaleString('de-DE') : '-'}
</span>
{val > 0 && renderGrowth(val, prevVal)}
</div>
<div className="flex flex-col items-center">
{spend > 0 && (
<div className="flex items-center gap-1">
<span className={`text-[11px] font-medium ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'text-amber-300' : 'text-indigo-400/80'}`}>
{spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
{renderGrowth(spend, prevSpend)}
</div>
)}
{row.gvByWeek?.[week] > 0 && (
<div className="flex items-center gap-1">
<span className={`text-[9px] font-black tracking-tighter ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'text-teal-300' : 'text-teal-500/70'}`}>
GV: {row.gvByWeek[week].toLocaleString('de-DE')}
</span>
{renderGrowth(row.gvByWeek[week], row.gvByWeek?.[weeks[idx + 1]] || 0)}
</div>
)}
</div>
</div>
Load More SKUs ({sortedRows.length - displayCount} remaining)
</button>
</td>
);
})}
</tr>
</tr>
)}
</>
);
})
})()
) : (
<tr>
<td colSpan={weeks.length + 1} className="p-10 text-center text-slate-500 italic text-base">