-
Notifications
You must be signed in to change notification settings - Fork 86
perf: parallelize page setup #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
const os = require('os') | ||
|
||
const fastq = require('fastq') | ||
|
||
const { processPage } = require('../pages/worker') | ||
|
||
// TODO: benchmark a large site to find a good value for this | ||
const PAGE_CONCURRENCY = 4 | ||
|
||
const runJobsQueue = (jobs) => { | ||
console.log(`Building ${jobs.length} pages`) | ||
|
||
const queue = fastq.promise(processPage, PAGE_CONCURRENCY) | ||
|
||
return Promise.all(jobs.map((job) => queue.push(job))) | ||
} | ||
|
||
module.exports = runJobsQueue |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,56 @@ | ||
const { join } = require('path') | ||
|
||
const asyncForEach = require('../../helpers/asyncForEach') | ||
const getFilePathForRoute = require('../../helpers/getFilePathForRoute') | ||
const isRouteWithFallback = require('../../helpers/isRouteWithFallback') | ||
const { logTitle, logItem } = require('../../helpers/logger') | ||
const setupNetlifyFunctionForPage = require('../../helpers/setupNetlifyFunctionForPage') | ||
const setupStaticFileForPage = require('../../helpers/setupStaticFileForPage') | ||
const { logTitle } = require('../../helpers/logger') | ||
|
||
const getPages = require('./pages') | ||
|
||
// Copy pre-rendered SSG pages | ||
const setup = async ({ functionsPath, publishPath }) => { | ||
logTitle('🔥 Copying pre-rendered pages with getStaticProps and JSON data to', publishPath) | ||
|
||
// Keep track of the functions that have been set up, so that we do not set up | ||
// a function for the same file path twice | ||
const filePathsDone = [] | ||
|
||
const filePathsDone = new Set() | ||
const pages = await getPages() | ||
|
||
await asyncForEach(pages, async ({ route, dataRoute, srcRoute }) => { | ||
logItem(route) | ||
|
||
// Copy pre-rendered HTML page | ||
const htmlPath = getFilePathForRoute(route, 'html') | ||
await setupStaticFileForPage({ inputPath: htmlPath, publishPath }) | ||
|
||
// Copy page's JSON data | ||
const jsonPath = getFilePathForRoute(route, 'json') | ||
await setupStaticFileForPage({ | ||
inputPath: jsonPath, | ||
outputPath: dataRoute, | ||
publishPath, | ||
}) | ||
|
||
// Set up the Netlify function (this is ONLY for preview mode) | ||
const relativePath = getFilePathForRoute(srcRoute || route, 'js') | ||
const filePath = join('pages', relativePath) | ||
|
||
// Skip if we have already set up a function for this file | ||
// or if the source route has a fallback (handled by getStaticPropsWithFallback) | ||
if (filePathsDone.includes(filePath) || (await isRouteWithFallback(srcRoute))) return | ||
|
||
logItem(filePath) | ||
await setupNetlifyFunctionForPage({ filePath, functionsPath }) | ||
filePathsDone.push(filePath) | ||
}) | ||
const jobs = [] | ||
|
||
await Promise.all( | ||
pages.map(async ({ route, dataRoute, srcRoute }) => { | ||
// Copy pre-rendered HTML page | ||
const htmlPath = getFilePathForRoute(route, 'html') | ||
|
||
jobs.push({ type: 'static', inputPath: htmlPath, publishPath }) | ||
|
||
// Copy page's JSON data | ||
const jsonPath = getFilePathForRoute(route, 'json') | ||
jobs.push({ | ||
type: 'static', | ||
inputPath: jsonPath, | ||
outputPath: dataRoute, | ||
publishPath, | ||
}) | ||
|
||
// Set up the Netlify function (this is ONLY for preview mode) | ||
const relativePath = getFilePathForRoute(srcRoute || route, 'js') | ||
const filePath = join('pages', relativePath) | ||
|
||
// Skip if we have already set up a function for this file | ||
|
||
if (filePathsDone.has(filePath)) { | ||
return | ||
} | ||
filePathsDone.add(filePath) | ||
|
||
// or if the source route has a fallback (handled by getStaticPropsWithFallback) | ||
if (await isRouteWithFallback(srcRoute)) { | ||
return | ||
} | ||
jobs.push({ type: 'function', filePath, functionsPath }) | ||
}), | ||
) | ||
return jobs | ||
} | ||
|
||
module.exports = setup |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const setupNetlifyFunctionForPage = require('../helpers/setupNetlifyFunctionForPage') | ||
const setupStaticFileForPage = require('../helpers/setupStaticFileForPage') | ||
|
||
exports.processPage = function processPage(job) { | ||
try { | ||
switch (job.type) { | ||
case 'function': { | ||
return setupNetlifyFunctionForPage(job) | ||
} | ||
case 'static': { | ||
return setupStaticFileForPage(job) | ||
} | ||
default: | ||
console.log('Unknown job type', job.type) | ||
} | ||
} catch (error) { | ||
console.error('Error in worker', error, 'Job', job) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
const runJobsQueue = require('../helpers/runJobsQueue') | ||
const apiSetup = require('../pages/api/setup') | ||
const getInitialPropsSetup = require('../pages/getInitialProps/setup') | ||
const getServerSidePropsSetup = require('../pages/getServerSideProps/setup') | ||
|
@@ -8,13 +9,17 @@ const withoutPropsSetup = require('../pages/withoutProps/setup') | |
|
||
// Set up all our NextJS pages according to the recipes defined in pages/ | ||
const setupPages = async ({ functionsPath, publishPath }) => { | ||
await apiSetup(functionsPath) | ||
await getInitialPropsSetup(functionsPath) | ||
await getServerSidePropsSetup(functionsPath) | ||
await getStaticPropsSetup({ functionsPath, publishPath }) | ||
await getSPFallbackSetup(functionsPath) | ||
await getSPRevalidateSetup(functionsPath) | ||
await withoutPropsSetup(publishPath) | ||
const jobs = [ | ||
...(await apiSetup(functionsPath)), | ||
...(await getInitialPropsSetup(functionsPath)), | ||
...(await getServerSidePropsSetup(functionsPath)), | ||
...(await getStaticPropsSetup({ functionsPath, publishPath })), | ||
...(await getSPFallbackSetup(functionsPath)), | ||
...(await getSPRevalidateSetup(functionsPath)), | ||
...(await withoutPropsSetup(publishPath)), | ||
] | ||
|
||
return runJobsQueue(jobs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this need to be returned? just curious There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because |
||
} | ||
|
||
module.exports = setupPages |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what's happening here - why do we have a map (which returns nothing) inside an awaited Promise.all, only to return
jobs
outside of it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The callback in the map is async (because it has to await
isRouteWithFallback
) , so it automatically returns a promise which resolves (tovoid
) when it's complete. This means that the map returns an array of promises (one for each page). Passing these to anawait
edPromise.all
means that it waits until all of the functions are complete. With an async callback like that, the map will immediately return the array of promises, before the callback has executed for each of the elements. It would be somehting like this. Apologies if you know all this:The reason I'm using the jobs array from outside of the map is so that I can conditionally add elements. If I returned them, I'd end up with an array with
undefined
elements for the pages we're skipping. We could then filter that array, but that means we'd need to loop through them again. Pushing onto an array outside means we only need to loop through once. I could usereduce
for the same effect, but I generally find that's harder to read.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ooh ok i dont think i processed that the map callback is async. ok wow this is wild!!!