diff --git a/.jules/bolt.md b/.jules/bolt.md index 9345ee7a..cf2dd851 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-31 - Optimize Next.js static generation builds with parallel data fetching + +**Learning:** Sequential `for...of` loops used for fetching dynamic page dependencies inside Next.js build-time functions like `generateStaticParams` (or `sitemap.ts`) block the thread and significantly increase total site build duration. +**Action:** When gathering parameters for static routes across large array sets (such as mapping years or categories), replace sequential `for...of` iteration with parallel execution using `Promise.all(array.map(...))`. Then, correctly aggregate nested outputs utilizing `.flat()` or `.flatMap()`, and preserve per-item `try/catch` fallbacks to prevent a single item failure from crashing the entire batch build. diff --git a/app/[year]/job-offers/[companyName]/page.tsx b/app/[year]/job-offers/[companyName]/page.tsx index a45ee546..23143704 100644 --- a/app/[year]/job-offers/[companyName]/page.tsx +++ b/app/[year]/job-offers/[companyName]/page.tsx @@ -20,19 +20,22 @@ export const revalidate = 3600; export async function generateStaticParams() { const years = getAvailableEditions(); - const params = []; - - for (const year of years) { - const companies = await getJobOffersForEdition(year); - for (const company of companies) { - params.push({ - year, - companyName: company.id, - }); - } - } - return params; + const nestedParams = await Promise.all( + years.map(async (year) => { + const params: { year: string; companyName: string }[] = []; + const companies = await getJobOffersForEdition(year); + for (const company of companies) { + params.push({ + year, + companyName: company.id, + }); + } + return params; + }) + ); + + return nestedParams.flat(); } export async function generateMetadata({ params }: CompanyJobOffersPageProps): Promise { diff --git a/app/[year]/speakers/[speakerId]/page.tsx b/app/[year]/speakers/[speakerId]/page.tsx index 62b234bd..1467d5b8 100644 --- a/app/[year]/speakers/[speakerId]/page.tsx +++ b/app/[year]/speakers/[speakerId]/page.tsx @@ -20,20 +20,23 @@ 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 nestedParams = await Promise.all( + years.map(async (year) => { + const params: { year: string; speakerId: string }[] = []; + try { + const speakers = await getSpeakers(year); + for (const speaker of speakers) { + params.push({ year, speakerId: speaker.id }); + } + } catch (error) { + console.warn(`Failed to fetch speakers for year ${year}:`, error); } - } catch (error) { - console.warn(`Failed to fetch speakers for year ${year}:`, error); - } - } + return params; + }) + ); - return params; + return nestedParams.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..e051dd7e 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 nestedParams = await Promise.all( + years.map(async (year) => { + const params: { year: string; tag: string }[] = []; + 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() }); + } + } catch (error) { + console.warn(`Failed to fetch talks for year ${year}:`, error); } - } catch (error) { - console.warn(`Failed to fetch talks for year ${year}:`, error); - } - } + return params; + }) + ); - return params; + return nestedParams.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 88f08655..e07ca1fb 100644 --- a/app/[year]/talks/[talkId]/page.tsx +++ b/app/[year]/talks/[talkId]/page.tsx @@ -29,21 +29,24 @@ 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 nestedParams = await Promise.all( + years.map(async (year) => { + const params: { year: string; talkId: string }[] = []; + try { + const sessionGroups = await getTalks(year); + const allTalks = sessionGroups.flatMap((group) => group.sessions); + for (const talk of allTalks) { + params.push({ year, talkId: talk.id }); + } + } catch (error) { + console.warn(`Failed to fetch talks for year ${year}:`, error); } - } catch (error) { - console.warn(`Failed to fetch talks for year ${year}:`, error); - } - } + return params; + }) + ); - return params; + return nestedParams.flat(); } export async function generateMetadata({ params }: TalkDetailProps): Promise {