diff --git a/.jules/bolt.md b/.jules/bolt.md index 9345ee7a..b3e847a9 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 - Parallelize generateStaticParams fetching + +**Learning:** In Next.js applications, sequential data fetching (using `for...of` loops) inside build-critical functions like `generateStaticParams` significantly slows down static page generation. The bottleneck is amplified when fetching data across multiple parameters (e.g., archived years). +**Action:** Replace sequential `for...of` loops with parallel execution using `Promise.all(array.map(...))` to fetch data concurrently. Preserve per-item `try/catch` blocks inside the map callback to prevent a single item's failure from prematurely failing the entire batch, returning an empty array `[]` as a fallback, and then use `.flat()` on the resulting nested array structure. diff --git a/app/[year]/speakers/[speakerId]/page.tsx b/app/[year]/speakers/[speakerId]/page.tsx index 62b234bd..e45658e2 100644 --- a/app/[year]/speakers/[speakerId]/page.tsx +++ b/app/[year]/speakers/[speakerId]/page.tsx @@ -20,20 +20,20 @@ interface SpeakerDetailProps { export async function generateStaticParams() { const years = getArchivedEditions(); - const params = []; - for (const year of years) { - try { - const speakers = await getSpeakers(year); - for (const speaker of speakers) { - params.push({ year, speakerId: speaker.id }); + const paramsArrays = await Promise.all( + years.map(async (year) => { + try { + const speakers = await getSpeakers(year); + return speakers.map((speaker) => ({ year, speakerId: speaker.id })); + } catch (error) { + console.warn(`Failed to fetch speakers for year ${year}:`, error); + return []; } - } catch (error) { - console.warn(`Failed to fetch speakers for year ${year}:`, error); - } - } + }) + ); - return params; + return paramsArrays.flat(); } export async function generateMetadata({ params }: SpeakerDetailProps): Promise { diff --git a/app/[year]/tags/[tag]/page.tsx b/app/[year]/tags/[tag]/page.tsx index 35c74f61..40082357 100644 --- a/app/[year]/tags/[tag]/page.tsx +++ b/app/[year]/tags/[tag]/page.tsx @@ -18,27 +18,30 @@ export const dynamicParams = false; export async function generateStaticParams() { const years = getArchivedEditions(); - const params = []; - for (const year of years) { - try { - const sessionGroups = await getTalks(year); - const allTalks = sessionGroups.flatMap((group) => group.sessions); - const allTags = new Set(); - - for (const talk of allTalks) { - getTagsFromTalk(talk).forEach((tag) => allTags.add(tag)); - } - - for (const tag of allTags) { - params.push({ year, tag: tag.replaceAll(" ", "-").toLowerCase() }); + const paramsArrays = await Promise.all( + years.map(async (year) => { + try { + const sessionGroups = await getTalks(year); + const allTalks = sessionGroups.flatMap((group) => group.sessions); + const allTags = new Set(); + + for (const talk of allTalks) { + getTagsFromTalk(talk).forEach((tag) => allTags.add(tag)); + } + + return Array.from(allTags).map((tag) => ({ + year, + tag: tag.replaceAll(" ", "-").toLowerCase(), + })); + } catch (error) { + console.warn(`Failed to fetch talks for year ${year}:`, error); + return []; } - } catch (error) { - console.warn(`Failed to fetch talks for year ${year}:`, error); - } - } + }) + ); - return params; + return paramsArrays.flat(); } export async function generateMetadata({ params }: Readonly): Promise { diff --git a/app/[year]/talks/[talkId]/page.tsx b/app/[year]/talks/[talkId]/page.tsx index 4aecf5a4..a879da05 100644 --- a/app/[year]/talks/[talkId]/page.tsx +++ b/app/[year]/talks/[talkId]/page.tsx @@ -30,21 +30,21 @@ interface TalkDetailProps { export async function generateStaticParams() { const years = getArchivedEditions(); - const params = []; - - for (const year of years) { - try { - const sessionGroups = await getTalks(year); - const allTalks = sessionGroups.flatMap((group) => group.sessions); - for (const talk of allTalks) { - params.push({ year, talkId: talk.id }); + + const paramsArrays = await Promise.all( + years.map(async (year) => { + try { + const sessionGroups = await getTalks(year); + const allTalks = sessionGroups.flatMap((group) => group.sessions); + return allTalks.map((talk) => ({ year, talkId: talk.id })); + } catch (error) { + console.warn(`Failed to fetch talks for year ${year}:`, error); + return []; } - } catch (error) { - console.warn(`Failed to fetch talks for year ${year}:`, error); - } - } + }) + ); - return params; + return paramsArrays.flat(); } export async function generateMetadata({ params }: TalkDetailProps): Promise {