diff --git a/.jules/bolt.md b/.jules/bolt.md index 9345ee7a..19ed7cf7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -2,3 +2,8 @@ **Learning:** In Next.js/React applications, when grouping items (like schedules or talks) into a `Map` where the values are arrays, using the array spread operator `[...existing, item]` inside a loop (like `forEach` or `map`) causes amortized O(N^2) memory allocations and unnecessary Garbage Collection overhead. **Action:** Always use `.push()` on the existing array reference if the data structure permits local mutation. For strict ESLint configurations enforcing `no-restricted-syntax`, extract the existing array, push to it, and handle the fallback elegantly (`if (!existing) { map.set(key, [item]); } else { existing.push(item); }`). + +## 2024-05-19 - Avoid redundant array string mapping in filtering loops + +**Learning:** When matching string parameters (like a slugified `tag`) against an array of objects where each object contains an array of strings (like `tags`), pulling the parameter transformation (`decodedTag.toLowerCase()`) out of the loop and using `reduce` or localized `for...of` loops prevents O(N\*M) redundant string `.toLowerCase()` and `.replaceAll()` reallocations and improves both rendering and static site generation performance. +**Action:** When filtering objects by matching sub-properties against a parameter, extract the invariant transformation logic from the loop, and try to construct the derived `displayTag` variable and the `filteredTalks` array in a single loop traversal. diff --git a/app/2026/tags/[tag]/page.tsx b/app/2026/tags/[tag]/page.tsx index 5c9a6ebc..bbc1cc87 100644 --- a/app/2026/tags/[tag]/page.tsx +++ b/app/2026/tags/[tag]/page.tsx @@ -40,8 +40,16 @@ export async function generateMetadata({ params }: { params: Promise<{ tag: stri const sessionGroups = await getTalks(year); const allTalks = sessionGroups.flatMap((group) => group.sessions); - const displayTag = - allTalks.flatMap(getTagsFromTalk).find((t) => t.replaceAll(" ", "-").toLowerCase() === decodedTag.toLowerCase()) ?? decodedTag.replaceAll("-", " "); + const targetTag = decodedTag.toLowerCase(); + + const foundDisplayTag = allTalks.reduce((acc, talk) => { + if (acc) return acc; + const talkTags = getTagsFromTalk(talk); + const matchedTag = talkTags.find((t) => t.replaceAll(" ", "-").toLowerCase() === targetTag); + return matchedTag ?? null; + }, null); + + const displayTag = foundDisplayTag ?? decodedTag.replaceAll("-", " "); return { title: `Talks tagged "${displayTag}" - DevBcn ${year}`, @@ -58,14 +66,21 @@ export default async function Page({ params }: { params: Promise<{ tag: string } const sessionGroups = await getTalks(year); const allTalks = sessionGroups.flatMap((group) => group.sessions); - const displayTag = - allTalks.flatMap(getTagsFromTalk).find((t) => t.replaceAll(" ", "-").toLowerCase() === decodedTag.toLowerCase()) ?? decodedTag.replaceAll("-", " "); + const targetTag = decodedTag.toLowerCase(); + const filteredTalks: typeof allTalks = []; - const filteredTalks = allTalks.filter((talk) => { + const foundDisplayTag = allTalks.reduce((acc, talk) => { const talkTags = getTagsFromTalk(talk); + const matchedTag = talkTags.find((t) => t.replaceAll(" ", "-").toLowerCase() === targetTag); + + if (matchedTag) { + filteredTalks.push(talk); + } + + return acc ?? matchedTag ?? null; + }, null); - return talkTags.some((t) => t.replaceAll(" ", "-").toLowerCase() === decodedTag.toLowerCase()); - }); + const displayTag = foundDisplayTag ?? decodedTag.replaceAll("-", " "); if (filteredTalks.length === 0) { notFound(); diff --git a/app/[year]/tags/[tag]/page.tsx b/app/[year]/tags/[tag]/page.tsx index 35c74f61..ec031d20 100644 --- a/app/[year]/tags/[tag]/page.tsx +++ b/app/[year]/tags/[tag]/page.tsx @@ -47,8 +47,16 @@ export async function generateMetadata({ params }: Readonly): Prom const sessionGroups = await getTalks(year); const allTalks = sessionGroups.flatMap((group) => group.sessions); - const displayTag = - allTalks.flatMap(getTagsFromTalk).find((t) => t.replaceAll(" ", "-").toLowerCase() === decodedTag.toLowerCase()) ?? decodedTag.replaceAll("-", " "); + const targetTag = decodedTag.toLowerCase(); + + const foundDisplayTag = allTalks.reduce((acc, talk) => { + if (acc) return acc; + const talkTags = getTagsFromTalk(talk); + const matchedTag = talkTags.find((t) => t.replaceAll(" ", "-").toLowerCase() === targetTag); + return matchedTag ?? null; + }, null); + + const displayTag = foundDisplayTag ?? decodedTag.replaceAll("-", " "); return { title: `Talks tagged "${displayTag}" - DevBcn ${year}`, @@ -64,14 +72,21 @@ export default async function TagPage({ params }: Readonly) { const sessionGroups = await getTalks(year); const allTalks = sessionGroups.flatMap((group) => group.sessions); - const displayTag = - allTalks.flatMap(getTagsFromTalk).find((t) => t.replaceAll(" ", "-").toLowerCase() === decodedTag.toLowerCase()) ?? decodedTag.replaceAll("-", " "); + const targetTag = decodedTag.toLowerCase(); + const filteredTalks: typeof allTalks = []; - const filteredTalks = allTalks.filter((talk) => { + const foundDisplayTag = allTalks.reduce((acc, talk) => { const talkTags = getTagsFromTalk(talk); + const matchedTag = talkTags.find((t) => t.replaceAll(" ", "-").toLowerCase() === targetTag); + + if (matchedTag) { + filteredTalks.push(talk); + } + + return acc ?? matchedTag ?? null; + }, null); - return talkTags.some((t) => t.replaceAll(" ", "-").toLowerCase() === decodedTag.toLowerCase()); - }); + const displayTag = foundDisplayTag ?? decodedTag.replaceAll("-", " "); if (filteredTalks.length === 0) { notFound();