Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strip-prerender-styles-ssr-manifest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Removes inline CSS for prerendered routes from the SSR manifest. The static HTML on disk already inlines those styles, and the SSR worker never renders prerendered routes, so the data was dead weight. Builds with many prerendered routes and `build.inlineStylesheets: "always"` (or `"auto"` with small stylesheets) will see a smaller SSR entry chunk, which reduces cold-start parse time on platforms like Cloudflare Workers.
23 changes: 22 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,12 @@ export async function manifestBuildPostHook(
? internals.middlewareEntryPoint
: undefined,
});
const code = injectManifest(manifest, ssrManifestChunk.code);
// Prerendered routes' styles are dead weight in the SSR manifest: the static
// HTML on disk already has them inlined, and the SSR worker never renders
// these routes. Stripping keeps the entry chunk small on platforms like
// Cloudflare Workers that re-parse it on every cold isolate start.
const ssrManifest = stripPrerenderedRouteStyles(manifest);
const code = injectManifest(ssrManifest, ssrManifestChunk.code);
mutate(ssrManifestChunk.fileName, code, false);
}

Expand Down Expand Up @@ -150,6 +155,22 @@ function injectManifest(manifest: SerializedSSRManifest, code: string) {
});
}

/**
* Returns a copy of the manifest with `styles` cleared on every prerendered
* route. Inline CSS for prerendered routes is dead weight in the SSR manifest:
* the prerendered HTML on disk already contains the `<style>` tags, and the
* SSR worker never renders these routes.
*/
function stripPrerenderedRouteStyles(manifest: SerializedSSRManifest): SerializedSSRManifest {
let stripped = false;
const routes = manifest.routes.map((route) => {
if (!route.routeData.prerender || route.styles.length === 0) return route;
stripped = true;
return { ...route, styles: [] };
});
return stripped ? { ...manifest, routes } : manifest;
}

async function buildManifest(
opts: StaticBuildOptions,
internals: BuildInternals,
Expand Down
61 changes: 61 additions & 0 deletions packages/astro/test/ssr-prerender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,67 @@ describe('SSR: prerender', () => {
});
});

describe('SSR manifest does not include inline CSS for prerendered routes', () => {
let fixture: Fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-prerender/',
output: 'server',
outDir: './dist/inline-stylesheets',
adapter: testAdapter(),
build: {
inlineStylesheets: 'always',
},
});
await fixture.build();
});

it('prerendered routes have empty styles in the SSR manifest', async () => {
const app = await fixture.loadTestAdapterApp();
const prerenderedRoutes = app.manifest.routes.filter((r) => r.routeData.prerender);
assert.ok(prerenderedRoutes.length > 0, 'fixture must have prerendered routes');
for (const route of prerenderedRoutes) {
assert.deepEqual(
route.styles,
[],
`route ${route.routeData.route} should have no styles in the SSR manifest`,
);
}
});

it('SSR routes still have inline styles in the SSR manifest', async () => {
const app = await fixture.loadTestAdapterApp();
const ssrRoute = app.manifest.routes.find((r) => r.routeData.route === '/not-prerendered');
assert.ok(ssrRoute, 'expected /not-prerendered route');
const hasInline = ssrRoute.styles.some(
(s) => s.type === 'inline' && s.content.includes('ssr-only'),
);
assert.ok(hasInline, 'SSR route should retain its inline CSS in the manifest');
});

it('prerendered HTML on disk still contains inline <style> tags', async () => {
const html = await fixture.readFile('/client/static/index.html');
const $ = cheerio.load(html);
const inlineStyles = $('style')
.map((_, el) => $(el).text() ?? '')
.toArray()
.join('');
assert.ok(
inlineStyles.includes('prerender-only'),
'prerendered HTML should still inline the route CSS',
);
});

it('SSR entry chunk does not contain the prerender-only CSS string', async () => {
const entry = (await fixture.readFile('/server/entry.mjs')).toString();
assert.ok(
!entry.includes('prerender-only'),
'SSR entry should not carry inline CSS for prerendered routes',
);
});
});

// NOTE: This test doesn't make sense as it relies on the fact that on the client build,
// you can change the prerender state of pages from the SSR build, however, the client build
// is not always guaranteed to run. If we want to support this feature, we may want to only allow
Expand Down
Loading