Skip to content

Commit

Permalink
Refactor prerendering chunk handling (#11245)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy authored Jun 17, 2024
1 parent 68f1d0d commit e22be22
Show file tree
Hide file tree
Showing 33 changed files with 692 additions and 193 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-boxes-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro": patch
---

Refactors prerendering chunk handling to correctly remove unused code during the SSR runtime
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ Any tests for `astro build` output should use the main `mocha` tests rather than

If a test needs to validate what happens on the page after it's loading in the browser, that's a perfect use for E2E dev server tests, i.e. to verify that hot-module reloading works in `astro dev` or that components were client hydrated and are interactive.

#### Creating tests

When creating new tests, it's best to reference other existing test files and replicate the same setup. Some other tips include:

- When re-using a fixture multiple times with different configurations, you should also configure unique `outDir`, `build.client`, and `build.server` values so the build output runtime isn't cached and shared by ESM between test runs.

### Other useful commands

```shell
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ export interface BuildInternals {
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
componentMetadata: SSRResult['componentMetadata'];
middlewareEntryPoint?: URL;

/**
* Chunks in the bundle that are only used in prerendering that we can delete later
*/
prerenderOnlyChunks: Rollup.OutputChunk[];
}

/**
Expand Down Expand Up @@ -151,6 +156,7 @@ export function createBuildInternals(): BuildInternals {
ssrSplitEntryChunks: new Map(),
entryPoints: new Map(),
cacheManifestUsed: false,
prerenderOnlyChunks: [],
};
}

Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/build/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export async function collectPagesData(
moduleSpecifier: '',
styles: [],
hoistedScript: undefined,
hasSharedModules: false,
};

clearInterval(routeCollectionLogTimeout);
Expand All @@ -80,7 +79,6 @@ export async function collectPagesData(
moduleSpecifier: '',
styles: [],
hoistedScript: undefined,
hasSharedModules: false,
};
}

Expand Down
9 changes: 9 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export function vitePluginChunks(): VitePlugin {
if (id.includes('astro/dist/runtime/server/')) {
return 'astro/server';
}
// Split the Astro runtime into a separate chunk for readability
if (id.includes('astro/dist/runtime')) {
return 'astro';
}
// Place `astro/env/setup` import in its own chunk to prevent Rollup's TLA bug
// https://github.com/rollup/rollup/issues/4708
if (id.includes('astro/dist/env/setup')) {
return 'astro/env-setup';
}
},
});
},
Expand Down
160 changes: 89 additions & 71 deletions packages/astro/src/core/build/plugins/plugin-prerender.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,105 @@
import path from 'node:path';
import type { Plugin as VitePlugin } from 'vite';
import type { Rollup, Plugin as VitePlugin } from 'vite';
import { getPrerenderMetadata } from '../../../prerender/metadata.js';
import type { BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
import { extendManualChunks } from './util.js';
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugin-pages.js';
import { getPagesFromVirtualModulePageName } from './util.js';

function vitePluginPrerender(opts: StaticBuildOptions, internals: BuildInternals): VitePlugin {
function vitePluginPrerender(internals: BuildInternals): VitePlugin {
return {
name: 'astro:rollup-plugin-prerender',

outputOptions(outputOptions) {
extendManualChunks(outputOptions, {
after(id, meta) {
// Split the Astro runtime into a separate chunk for readability
if (id.includes('astro/dist/runtime')) {
return 'astro';
}
const pageInfo = internals.pagesByViteID.get(id);
let hasSharedModules = false;
if (pageInfo) {
// prerendered pages should be split into their own chunk
// Important: this can't be in the `pages/` directory!
if (getPrerenderMetadata(meta.getModuleInfo(id)!)) {
const infoMeta = meta.getModuleInfo(id)!;
generateBundle(_, bundle) {
const moduleIds = this.getModuleIds();
for (const id of moduleIds) {
const pageInfo = internals.pagesByViteID.get(id);
if (!pageInfo) continue;
const moduleInfo = this.getModuleInfo(id);
if (!moduleInfo) continue;

// Here, we check if this page is importing modules that are shared among other modules e.g. middleware, other SSR pages, etc.
// we loop the modules that the current page imports
for (const moduleId of infoMeta.importedIds) {
// we retrieve the metadata of the module
const moduleMeta = meta.getModuleInfo(moduleId)!;
if (
// a shared modules should be inside the `src/` folder, at least
moduleMeta.id.startsWith(opts.settings.config.srcDir.pathname) &&
// and has at least two importers: the current page and something else
moduleMeta.importers.length > 1
) {
// Now, we have to trace back the modules imported and analyze them;
// understanding if a module is eventually shared between two pages isn't easy, because a module could
// be imported by a page and a component that is eventually imported by a page.
//
// Given the previous statement, we only check if
// - the module is a page, and it's not pre-rendered
// - the module is the middleware
// If one of these conditions is met, we need a separate chunk
for (const importer of moduleMeta.importedIds) {
// we don't want to analyze the same module again, so we skip it
if (importer !== id) {
const importerModuleMeta = meta.getModuleInfo(importer);
if (importerModuleMeta) {
// if the module is inside the pages
if (importerModuleMeta.id.includes('/pages')) {
// we check if it's not pre-rendered
if (getPrerenderMetadata(importerModuleMeta) === false) {
hasSharedModules = true;
break;
}
}
// module isn't an Astro route/page, it could be a middleware
else if (importerModuleMeta.id.includes('/middleware')) {
hasSharedModules = true;
break;
}
}
}
}
}
}
const prerender = !!getPrerenderMetadata(moduleInfo);
pageInfo.route.prerender = prerender;
}

pageInfo.hasSharedModules = hasSharedModules;
pageInfo.route.prerender = true;
return 'prerender';
}
pageInfo.route.prerender = false;
// dynamic pages should all go in their own chunk in the pages/* directory
return `pages/${path.basename(pageInfo.component)}`;
}
},
});
// Find all chunks used in the SSR runtime (that aren't used for prerendering only), then use
// the Set to find the inverse, where chunks that are only used for prerendering. It's faster
// to compute `internals.prerenderOnlyChunks` this way. The prerendered chunks will be deleted
// after we finish prerendering.
const nonPrerenderOnlyChunks = getNonPrerenderOnlyChunks(bundle, internals);
internals.prerenderOnlyChunks = Object.values(bundle).filter((chunk) => {
return chunk.type === 'chunk' && !nonPrerenderOnlyChunks.has(chunk);
}) as Rollup.OutputChunk[];
},
};
}

function getNonPrerenderOnlyChunks(bundle: Rollup.OutputBundle, internals: BuildInternals) {
const chunks = Object.values(bundle);

const prerenderOnlyEntryChunks = new Set<Rollup.OutputChunk>();
const nonPrerenderOnlyEntryChunks = new Set<Rollup.OutputChunk>();
for (const chunk of chunks) {
if (chunk.type === 'chunk' && (chunk.isEntry || chunk.isDynamicEntry)) {
// See if this entry chunk is prerendered, if so, skip it
if (chunk.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
const pageDatas = getPagesFromVirtualModulePageName(
internals,
ASTRO_PAGE_RESOLVED_MODULE_ID,
chunk.facadeModuleId
);
const prerender = pageDatas.every((pageData) => pageData.route.prerender);
if (prerender) {
prerenderOnlyEntryChunks.add(chunk);
continue;
}
}
// Ideally we should record entries when `functionPerRoute` is enabled, but this breaks some tests
// that expect the entrypoint to still exist even if it should be unused.
// TODO: Revisit this so we can delete additional unused chunks
// else if (chunk.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
// const pageDatas = getPagesFromVirtualModulePageName(
// internals,
// RESOLVED_SPLIT_MODULE_ID,
// chunk.facadeModuleId
// );
// const prerender = pageDatas.every((pageData) => pageData.route.prerender);
// if (prerender) {
// prerenderOnlyEntryChunks.add(chunk);
// continue;
// }
// }

nonPrerenderOnlyEntryChunks.add(chunk);
}
}

// From the `nonPrerenderedEntryChunks`, we crawl all the imports/dynamicImports to find all
// other chunks that are use by the non-prerendered runtime
const nonPrerenderOnlyChunks = new Set(nonPrerenderOnlyEntryChunks);
for (const chunk of nonPrerenderOnlyChunks) {
for (const importFileName of chunk.imports) {
const importChunk = bundle[importFileName];
if (importChunk?.type === 'chunk') {
nonPrerenderOnlyChunks.add(importChunk);
}
}
for (const dynamicImportFileName of chunk.dynamicImports) {
const dynamicImportChunk = bundle[dynamicImportFileName];
// The main server entry (entry.mjs) may import a prerender-only entry chunk, we skip in this case
// to prevent incorrectly marking it as non-prerendered.
if (
dynamicImportChunk?.type === 'chunk' &&
!prerenderOnlyEntryChunks.has(dynamicImportChunk)
) {
nonPrerenderOnlyChunks.add(dynamicImportChunk);
}
}
}

return nonPrerenderOnlyChunks;
}

export function pluginPrerender(
opts: StaticBuildOptions,
internals: BuildInternals
Expand All @@ -96,7 +114,7 @@ export function pluginPrerender(
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginPrerender(opts, internals),
vitePlugin: vitePluginPrerender(internals),
};
},
},
Expand Down
31 changes: 18 additions & 13 deletions packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ function vitePluginSSR(
name: '@astrojs/vite-plugin-astro-ssr-server',
enforce: 'post',
options(opts) {
return addRollupInput(opts, [SSR_VIRTUAL_MODULE_ID]);
const inputs = new Set<string>();

for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}
inputs.add(getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, pageData.component));
}

inputs.add(SSR_VIRTUAL_MODULE_ID);
return addRollupInput(opts, Array.from(inputs));
},
resolveId(id) {
if (id === SSR_VIRTUAL_MODULE_ID) {
Expand Down Expand Up @@ -72,7 +82,6 @@ function vitePluginSSR(
contents.push(...ssrCode.contents);
return [...imports, ...contents, ...exports].join('\n');
}
return void 0;
},
async generateBundle(_opts, bundle) {
// Add assets from this SSR chunk as well.
Expand Down Expand Up @@ -141,23 +150,20 @@ function vitePluginSSRSplit(
adapter: AstroAdapter,
options: StaticBuildOptions
): VitePlugin {
const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter);
return {
name: '@astrojs/vite-plugin-astro-ssr-split',
enforce: 'post',
options(opts) {
if (functionPerRouteEnabled) {
const inputs = new Set<string>();
const inputs = new Set<string>();

for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}

return addRollupInput(opts, Array.from(inputs));
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
}

return addRollupInput(opts, Array.from(inputs));
},
resolveId(id) {
if (id.startsWith(SPLIT_MODULE_ID)) {
Expand Down Expand Up @@ -185,7 +191,6 @@ function vitePluginSSRSplit(

return [...imports, ...contents, ...exports].join('\n');
}
return void 0;
},
async generateBundle(_opts, bundle) {
// Add assets from this SSR chunk as well.
Expand Down
Loading

0 comments on commit e22be22

Please sign in to comment.