Improve static build performance on large sites (#2391)
* Improve static build performance on large sites * Changeset * Remove debugging code * Pass through the rss fn
This commit is contained in:
parent
5d6b29ae37
commit
c8a257adc4
6 changed files with 128 additions and 34 deletions
5
.changeset/metal-pens-decide.md
Normal file
5
.changeset/metal-pens-decide.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improvements performance for building sites with thousands of pages with the static build
|
|
@ -177,7 +177,11 @@ export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: str
|
||||||
rss?: (...args: any[]) => any;
|
rss?: (...args: any[]) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetStaticPathsResult = { params: Params; props?: Props }[];
|
export type GetStaticPathsItem = { params: Params; props?: Props };
|
||||||
|
export type GetStaticPathsResult = GetStaticPathsItem[];
|
||||||
|
export type GetStaticPathsResultKeyed = GetStaticPathsResult & {
|
||||||
|
keyed: Map<string, GetStaticPathsItem>
|
||||||
|
};
|
||||||
|
|
||||||
export interface HydrateOptions {
|
export interface HydrateOptions {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
@ -313,7 +317,7 @@ export interface RouteData {
|
||||||
type: 'page';
|
type: 'page';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteCache = Record<string, GetStaticPathsResult>;
|
export type RouteCache = Record<string, GetStaticPathsResultKeyed>;
|
||||||
|
|
||||||
export type RuntimeMode = 'development' | 'production';
|
export type RuntimeMode = 'development' | 'production';
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { preload as ssrPreload } from '../ssr/index.js';
|
||||||
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
|
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
|
||||||
import { generatePaginateFunction } from '../ssr/paginate.js';
|
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||||
import { generateRssFunction } from '../ssr/rss.js';
|
import { generateRssFunction } from '../ssr/rss.js';
|
||||||
|
import { assignStaticPaths } from '../ssr/route-cache.js';
|
||||||
|
|
||||||
export interface CollectPagesDataOptions {
|
export interface CollectPagesDataOptions {
|
||||||
astroConfig: AstroConfig;
|
astroConfig: AstroConfig;
|
||||||
|
@ -112,8 +113,8 @@ async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: Rout
|
||||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||||
validateGetStaticPathsModule(mod);
|
validateGetStaticPathsModule(mod);
|
||||||
const rss = generateRssFunction(astroConfig.buildOptions.site, route);
|
const rss = generateRssFunction(astroConfig.buildOptions.site, route);
|
||||||
const staticPaths: GetStaticPathsResult = (await mod.getStaticPaths!({ paginate: generatePaginateFunction(route), rss: rss.generator })).flat();
|
await assignStaticPaths(routeCache, route, mod, rss.generator);
|
||||||
routeCache[route.component] = staticPaths;
|
const staticPaths = routeCache[route.component];
|
||||||
validateGetStaticPathsResult(staticPaths, logging);
|
validateGetStaticPathsResult(staticPaths, logging);
|
||||||
return {
|
return {
|
||||||
paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean),
|
paths: staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean),
|
||||||
|
|
|
@ -31,6 +31,8 @@ export interface StaticBuildOptions {
|
||||||
viteConfig: ViteConfigWithSSR;
|
viteConfig: ViteConfigWithSSR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_CONCURRENT_RENDERS = 10;
|
||||||
|
|
||||||
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
||||||
const pathrepl = opts.astroConfig.buildOptions.pageUrlFormat === 'directory' ? '/index.html' : pathname === '/' ? 'index.html' : '.html';
|
const pathrepl = opts.astroConfig.buildOptions.pageUrlFormat === 'directory' ? '/index.html' : pathname === '/' ? 'index.html' : '.html';
|
||||||
opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, ''));
|
opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, ''));
|
||||||
|
@ -45,6 +47,29 @@ function chunkIsPage(output: OutputAsset | OutputChunk, internals: BuildInternal
|
||||||
return chunk.facadeModuleId && (internals.entrySpecifierToBundleMap.has(chunk.facadeModuleId) || internals.entrySpecifierToBundleMap.has('/' + chunk.facadeModuleId));
|
return chunk.facadeModuleId && (internals.entrySpecifierToBundleMap.has(chunk.facadeModuleId) || internals.entrySpecifierToBundleMap.has('/' + chunk.facadeModuleId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Throttle the rendering a paths to prevents creating too many Promises on the microtask queue.
|
||||||
|
function *throttle(max: number, inPaths: string[]) {
|
||||||
|
let tmp = [];
|
||||||
|
let i = 0;
|
||||||
|
for(let path of inPaths) {
|
||||||
|
tmp.push(path);
|
||||||
|
if(i === max) {
|
||||||
|
yield tmp;
|
||||||
|
// Empties the array, to avoid allocating a new one.
|
||||||
|
tmp.length = 0;
|
||||||
|
i = 0;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If tmp has items in it, that means there were less than {max} paths remaining
|
||||||
|
// at the end, so we need to yield these too.
|
||||||
|
if(tmp.length) {
|
||||||
|
yield tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function staticBuild(opts: StaticBuildOptions) {
|
export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, astroConfig } = opts;
|
const { allPages, astroConfig } = opts;
|
||||||
|
|
||||||
|
@ -246,10 +271,17 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
|
||||||
renderers,
|
renderers,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPromises = pageData.paths.map((path) => {
|
const renderPromises = [];
|
||||||
return generatePath(path, opts, generationOptions);
|
// Throttle the paths to avoid overloading the CPU with too many tasks.
|
||||||
});
|
for(const paths of throttle(MAX_CONCURRENT_RENDERS, pageData.paths)) {
|
||||||
return await Promise.all(renderPromises);
|
for(const path of paths) {
|
||||||
|
renderPromises.push(generatePath(path, opts, generationOptions));
|
||||||
|
}
|
||||||
|
// This blocks generating more paths until these 10 complete.
|
||||||
|
await Promise.all(renderPromises);
|
||||||
|
// This empties the array without allocating a new one.
|
||||||
|
renderPromises.length = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GeneratePathOptions {
|
interface GeneratePathOptions {
|
||||||
|
@ -276,6 +308,9 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
||||||
logging,
|
logging,
|
||||||
pathname,
|
pathname,
|
||||||
mod,
|
mod,
|
||||||
|
// Do not validate as validation already occurred for static routes
|
||||||
|
// and validation is relatively expensive.
|
||||||
|
validate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
debug(logging, 'generate', `Generating: ${pathname}`);
|
debug(logging, 'generate', `Generating: ${pathname}`);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { BuildResult } from 'esbuild';
|
import type { BuildResult } from 'esbuild';
|
||||||
import type vite from '../vite';
|
import type vite from '../vite';
|
||||||
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, Renderer, RouteCache, RouteData, RuntimeMode, SSRElement, SSRError } from '../../@types/astro';
|
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, GetStaticPathsResultKeyed, Params, Props, Renderer, RouteCache, RouteData, RuntimeMode, SSRElement, SSRError } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
|
|
||||||
import eol from 'eol';
|
import eol from 'eol';
|
||||||
|
@ -14,6 +14,7 @@ import { injectTags } from './html.js';
|
||||||
import { generatePaginateFunction } from './paginate.js';
|
import { generatePaginateFunction } from './paginate.js';
|
||||||
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
|
import { assignStaticPaths, ensureRouteCached, findPathItemByKey } from './route-cache.js';
|
||||||
|
|
||||||
const svelteStylesRE = /svelte\?svelte&type=style/;
|
const svelteStylesRE = /svelte\?svelte&type=style/;
|
||||||
|
|
||||||
|
@ -131,16 +132,18 @@ export async function getParamsAndProps({
|
||||||
logging,
|
logging,
|
||||||
pathname,
|
pathname,
|
||||||
mod,
|
mod,
|
||||||
|
validate = true
|
||||||
}: {
|
}: {
|
||||||
route: RouteData | undefined;
|
route: RouteData | undefined;
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
validate?: boolean;
|
||||||
}): Promise<[Params, Props]> {
|
}): Promise<[Params, Props]> {
|
||||||
// Handle dynamic routes
|
// Handle dynamic routes
|
||||||
let params: Params = {};
|
let params: Params = {};
|
||||||
let pageProps: Props = {};
|
let pageProps: Props;
|
||||||
if (route && !route.pathname) {
|
if (route && !route.pathname) {
|
||||||
if (route.params.length) {
|
if (route.params.length) {
|
||||||
const paramsMatch = route.pattern.exec(pathname);
|
const paramsMatch = route.pattern.exec(pathname);
|
||||||
|
@ -148,24 +151,27 @@ export async function getParamsAndProps({
|
||||||
params = getParams(route.params)(paramsMatch);
|
params = getParams(route.params)(paramsMatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateGetStaticPathsModule(mod);
|
if(validate) {
|
||||||
if (!routeCache[route.component]) {
|
validateGetStaticPathsModule(mod);
|
||||||
routeCache[route.component] = await (
|
|
||||||
await mod.getStaticPaths!({
|
|
||||||
paginate: generatePaginateFunction(route),
|
|
||||||
rss: () => {
|
|
||||||
/* noop */
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).flat();
|
|
||||||
}
|
}
|
||||||
validateGetStaticPathsResult(routeCache[route.component], logging);
|
if (!routeCache[route.component]) {
|
||||||
const routePathParams: GetStaticPathsResult = routeCache[route.component];
|
await assignStaticPaths(routeCache, route, mod);
|
||||||
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
}
|
||||||
|
if(validate) {
|
||||||
|
// This validation is expensive so we only want to do it in dev.
|
||||||
|
validateGetStaticPathsResult(routeCache[route.component], logging);
|
||||||
|
}
|
||||||
|
const staticPaths: GetStaticPathsResultKeyed = routeCache[route.component];
|
||||||
|
const paramsKey = JSON.stringify(params);
|
||||||
|
const matchedStaticPath = findPathItemByKey(staticPaths, paramsKey, logging);
|
||||||
if (!matchedStaticPath) {
|
if (!matchedStaticPath) {
|
||||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||||
}
|
}
|
||||||
pageProps = { ...matchedStaticPath.props } || {};
|
// This is written this way for performance; instead of spreading the props
|
||||||
|
// which is O(n), create a new object that extends props.
|
||||||
|
pageProps = Object.create(matchedStaticPath.props || Object.prototype);
|
||||||
|
} else {
|
||||||
|
pageProps = {};
|
||||||
}
|
}
|
||||||
return [params, pageProps];
|
return [params, pageProps];
|
||||||
}
|
}
|
||||||
|
@ -185,16 +191,7 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateGetStaticPathsModule(mod);
|
validateGetStaticPathsModule(mod);
|
||||||
if (!routeCache[route.component]) {
|
await ensureRouteCached(routeCache, route, mod);
|
||||||
routeCache[route.component] = await (
|
|
||||||
await mod.getStaticPaths!({
|
|
||||||
paginate: generatePaginateFunction(route),
|
|
||||||
rss: () => {
|
|
||||||
/* noop */
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).flat();
|
|
||||||
}
|
|
||||||
validateGetStaticPathsResult(routeCache[route.component], logging);
|
validateGetStaticPathsResult(routeCache[route.component], logging);
|
||||||
const routePathParams: GetStaticPathsResult = routeCache[route.component];
|
const routePathParams: GetStaticPathsResult = routeCache[route.component];
|
||||||
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
||||||
|
|
52
packages/astro/src/core/ssr/route-cache.ts
Normal file
52
packages/astro/src/core/ssr/route-cache.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, RouteCache, RouteData } from '../../@types/astro';
|
||||||
|
import type { LogOptions } from '../logger';
|
||||||
|
|
||||||
|
import { debug } from '../logger.js';
|
||||||
|
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||||
|
|
||||||
|
type RSSFn = (...args: any[]) => any;
|
||||||
|
|
||||||
|
export async function callGetStaticPaths(mod: ComponentInstance, route: RouteData, rssFn?: RSSFn): Promise<GetStaticPathsResultKeyed> {
|
||||||
|
const staticPaths: GetStaticPathsResult = await (
|
||||||
|
await mod.getStaticPaths!({
|
||||||
|
paginate: generatePaginateFunction(route),
|
||||||
|
rss: rssFn || (() => {
|
||||||
|
/* noop */
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
).flat();
|
||||||
|
|
||||||
|
const keyedStaticPaths = staticPaths as GetStaticPathsResultKeyed;
|
||||||
|
keyedStaticPaths.keyed = new Map<string, GetStaticPathsItem>();
|
||||||
|
for(const sp of keyedStaticPaths) {
|
||||||
|
const paramsKey = JSON.stringify(sp.params);
|
||||||
|
keyedStaticPaths.keyed.set(paramsKey, sp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyedStaticPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assignStaticPaths(routeCache: RouteCache, route: RouteData, mod: ComponentInstance, rssFn?: RSSFn): Promise<void> {
|
||||||
|
const staticPaths = await callGetStaticPaths(mod, route, rssFn);
|
||||||
|
routeCache[route.component] = staticPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureRouteCached(routeCache: RouteCache, route: RouteData, mod: ComponentInstance, rssFn?: RSSFn): Promise<GetStaticPathsResultKeyed> {
|
||||||
|
if (!routeCache[route.component]) {
|
||||||
|
const staticPaths = await callGetStaticPaths(mod, route, rssFn);
|
||||||
|
routeCache[route.component] = staticPaths;
|
||||||
|
return staticPaths;
|
||||||
|
} else {
|
||||||
|
return routeCache[route.component];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPathItemByKey(staticPaths: GetStaticPathsResultKeyed, paramsKey: string, logging: LogOptions) {
|
||||||
|
let matchedStaticPath = staticPaths.keyed.get(paramsKey);
|
||||||
|
if(matchedStaticPath) {
|
||||||
|
return matchedStaticPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(logging, 'findPathItemByKey', `Unexpected cache miss looking for ${paramsKey}`);
|
||||||
|
matchedStaticPath = staticPaths.find(({ params: _params }) => JSON.stringify(_params) === paramsKey);
|
||||||
|
}
|
Loading…
Reference in a new issue