From bb227371143540782a9fc83b92f7813e1d5f129e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:18:05 +0000 Subject: [PATCH] perf: optimize platform stats computation to single O(n) pass - Consolidated multiple `.map().filter()` and `.flatMap()` calls in `src/hooks/use-platform-stats.ts` into a single loop. - Consolidated ~8 redundant iterations over the `profiles` array in `src/pages/about-page.tsx` into a single `for` loop. - Replaced secondary iterations over the arrays to calculate totals with size lookups on the generated sets/dictionaries. - Added a Bolt learning journal entry in `.jules/bolt.md`. Co-authored-by: KxlSys <116387953+KxlSys@users.noreply.github.com> --- .jules/bolt.md | 4 ++ src/hooks/use-platform-stats.ts | 27 ++++++++-- src/pages/about-page.tsx | 95 +++++++++++++++++++++------------ 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 125fd89..7f96196 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -17,3 +17,7 @@ ## 2024-06-25 - DebouncedInput Component Memoization **Learning:** The `DebouncedInput` component was used in several places to prevent excessive parent re-renders while typing. However, because `DebouncedInput` itself was not wrapped in `React.memo()`, it was still re-rendering unnecessarily whenever its parent components re-rendered for reasons unrelated to the input (e.g. other form fields changing, complex page state updates). This unnecessary re-rendering could destroy and recreate its internal debouncing logic and cause extra reconciliation work. **Action:** When creating utility wrapper components designed to optimize performance (like `DebouncedInput`), ensure the component itself is memoized with `React.memo()` so it fully shields itself from irrelevant parent state updates. + +## 2024-07-09 - Avoid Multiple Array Iterations for Data Aggregation +**Learning:** In functions like `computeStats` (in `about-page.tsx` and `use-platform-stats.ts`) that compute multiple aggregate metrics from an array of objects, chaining methods like `.map().filter()`, `.flatMap()`, or doing multiple explicit `for` loops across the entire dataset causes unnecessary intermediate array allocations and redundant O(n) passes. In extreme cases, doing this 8-10 times over a large dataset blocks the main thread. +**Action:** Always consolidate multiple map/reduce/filter operations into a single O(n) `for` loop. Keep multiple accumulator dictionaries/counters and update them all at once per item. Also, derive counts (like total distinct items) directly from the keys/size of the accumulators instead of recalculating them from the original array. diff --git a/src/hooks/use-platform-stats.ts b/src/hooks/use-platform-stats.ts index 20d9dee..a7ec27a 100644 --- a/src/hooks/use-platform-stats.ts +++ b/src/hooks/use-platform-stats.ts @@ -9,19 +9,36 @@ export interface PlatformStats { techCount: number; } +// ⚡ Bolt: Consolidated multiple array passes (.map.filter, .filter, .flatMap) into a single O(n) loop function computeStats( profiles: Array<{ city?: string | null; open_to_collaboration?: boolean | null; tech_stack?: string[] | null }> ): PlatformStats { const total = profiles.length; if (total === 0) return { totalUsers: 0, totalCities: 0, collaborationRate: 0, techCount: 0 }; - const cities = new Set(profiles.map((p) => p.city).filter(Boolean)).size; - const collab = profiles.filter((p) => p.open_to_collaboration).length; - const allTechs = new Set(profiles.flatMap((p) => p.tech_stack ?? [])).size; + + let collab = 0; + const citiesSet = new Set(); + const techsSet = new Set(); + + for (const p of profiles) { + if (p.open_to_collaboration) { + collab++; + } + if (p.city) { + citiesSet.add(p.city); + } + if (p.tech_stack) { + for (const tech of p.tech_stack) { + techsSet.add(tech); + } + } + } + return { totalUsers: total, - totalCities: cities, + totalCities: citiesSet.size, collaborationRate: Math.round((collab / total) * 100), - techCount: allTechs, + techCount: techsSet.size, }; } diff --git a/src/pages/about-page.tsx b/src/pages/about-page.tsx index c274a95..17b4469 100644 --- a/src/pages/about-page.tsx +++ b/src/pages/about-page.tsx @@ -105,6 +105,7 @@ const CATEGORY_COLORS: Record = { "Autres profils": "bg-chart-5", }; +// ⚡ Bolt: Consolidated multiple iterations into a single O(n) pass over profiles. function computeStats(profiles: RawProfile[]): ComputedStats { const total = profiles.length; if (total === 0) { @@ -123,9 +124,64 @@ function computeStats(profiles: RawProfile[]): ComputedStats { } const roleCounts: Record = {}; - for (const p of profiles) + const expCounts: Record = {}; + const techCounts: Record = {}; + const cityCounts: Record = {}; + + let collab = 0; + let activeLastWeek = 0; + + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + + const months = Array.from({ length: 6 }, (_, i) => { + const d = new Date(now.getFullYear(), now.getMonth() - 5 + i, 1); + return { + month: d.toLocaleDateString("fr-FR", { month: "short" }), + year: d.getFullYear(), + monthNum: d.getMonth(), + count: 0, + }; + }); + + // ⚡ Bolt: Single pass loop for all aggregations + for (const p of profiles) { + // Roles roleCounts[p.role_type] = (roleCounts[p.role_type] || 0) + 1; + // Experience + expCounts[p.experience_level] = (expCounts[p.experience_level] || 0) + 1; + + // Techs + if (p.tech_stack) { + for (const t of p.tech_stack) { + techCounts[t] = (techCounts[t] || 0) + 1; + } + } + + // Cities + if (p.city) { + cityCounts[p.city] = (cityCounts[p.city] || 0) + 1; + } + + // Growth + const d = new Date(p.created_at); + const entry = months.find( + (m) => m.year === d.getFullYear() && m.monthNum === d.getMonth() + ); + if (entry) entry.count++; + + // Collaboration + if (p.open_to_collaboration) { + collab++; + } + + // Active last week + if (p.last_seen_at && new Date(p.last_seen_at) >= weekAgo) { + activeLastWeek++; + } + } + // Regroupement dynamique : agrège tous les role_type connus dans des // catégories plus larges, n'affiche que celles ayant au moins 1 profil // et trie par population décroissante. @@ -143,9 +199,6 @@ function computeStats(profiles: RawProfile[]): ComputedStats { .filter((r) => r.pct > 0) .sort((a, b) => b.pct - a.pct); - const expCounts: Record = {}; - for (const p of profiles) - expCounts[p.experience_level] = (expCounts[p.experience_level] || 0) + 1; const experience = [ { label: "Junior", @@ -164,10 +217,6 @@ function computeStats(profiles: RawProfile[]): ComputedStats { }, ]; - const techCounts: Record = {}; - for (const p of profiles) - for (const t of p.tech_stack ?? []) - techCounts[t] = (techCounts[t] || 0) + 1; const techs = Object.entries(techCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 6) @@ -176,9 +225,6 @@ function computeStats(profiles: RawProfile[]): ComputedStats { pct: Math.round((count / total) * 100), })); - const cityCounts: Record = {}; - for (const p of profiles) - if (p.city) cityCounts[p.city] = (cityCounts[p.city] || 0) + 1; const sortedCities = Object.entries(cityCounts).sort((a, b) => b[1] - a[1]); const cities = sortedCities .slice(0, 3) @@ -187,36 +233,15 @@ function computeStats(profiles: RawProfile[]): ComputedStats { if (otherCount > 0) cities.push({ name: "Autres", pct: Math.round((otherCount / total) * 100) }); - const now = new Date(); - const months = Array.from({ length: 6 }, (_, i) => { - const d = new Date(now.getFullYear(), now.getMonth() - 5 + i, 1); - return { - month: d.toLocaleDateString("fr-FR", { month: "short" }), - year: d.getFullYear(), - monthNum: d.getMonth(), - count: 0, - }; - }); - for (const p of profiles) { - const d = new Date(p.created_at); - const entry = months.find( - (m) => m.year === d.getFullYear() && m.monthNum === d.getMonth() - ); - if (entry) entry.count++; - } const maxCount = Math.max(...months.map((m) => m.count), 1); const growth = months.map((m) => ({ month: m.month, value: Math.round((m.count / maxCount) * 100), })); - const collab = profiles.filter((p) => p.open_to_collaboration).length; - const totalCities = new Set(profiles.map((p) => p.city).filter(Boolean)).size; - const allTechs = new Set(profiles.flatMap((p) => p.tech_stack ?? [])).size; - const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - const activeLastWeek = profiles.filter( - (p) => p.last_seen_at && new Date(p.last_seen_at) >= weekAgo - ).length; + // ⚡ Bolt: Derive distinct counts from keys instead of doing additional passes + const totalCities = Object.keys(cityCounts).length; + const allTechs = Object.keys(techCounts).length; return { total,