import fs from 'node:fs/promises'; import { loremIpsumHtml } from './_util.js'; /** * Generates a benchmark project targeting specific rendering hot paths * identified in RENDERING_PERF_PLAN.md. Each page isolates a different * performance-sensitive pattern so we can measure the impact of optimizations. */ // --------------------------------------------------------------------------- // Components // --------------------------------------------------------------------------- const components = { // A leaf component with ~4 static HTML parts and a few expressions. // Used by many-components to stress markHTMLString + isHTMLString (#1, #2). 'components/Card.astro': `\ --- const { title, href, index } = Astro.props; ---

{title}

Card number {index}, rendering static content around expressions.

`, // A wrapper component that renders children via a default slot. // Used by many-slots to stress eager slot prerendering (#9). 'components/Section.astro': `\ --- const { heading } = Astro.props; ---

{heading}

`, // A component with 3 named slots — only default is typically used. // Stresses eager slot prerendering (#9). 'components/Layout.astro': `\ --- const { title } = Astro.props; --- {title}
`, // A component that contributes a `, // A component that contributes a unique `, }; // --------------------------------------------------------------------------- // Pages // --------------------------------------------------------------------------- const pages = { // #1, #2, #6: 200 Astro component instances, each with ~4 HTML parts. // Stresses markHTMLString allocs, isHTMLString checks, validateComponentProps. 'pages/many-components.astro': `\ --- import Card from '../components/Card.astro'; const items = Array.from({ length: 200 }, (_, i) => ({ title: \`Card \${i}\`, href: \`/card/\${i}\`, index: i, })); --- Many Components

200 Component Instances

{items.map((item) => ( ))} `, // #2, #5, #10: Thousands of text expressions ({value}). // Stresses renderChild dispatch ordering, isHTMLString, escapeHTML. 'pages/many-expressions.astro': `\ --- const items = Array.from({ length: 2000 }, (_, i) => ({ name: \`Item \${i}\`, value: i * 17, label: i % 2 === 0 ? "even" : "odd", })); const title = "Expression Heavy Page"; const subtitle = "Testing renderChild dispatch"; --- {title}

{title}

{subtitle}

{items.map((item) => ( ))}
NameValueLabel
{item.name} {item.value} {item.label}
`, // #3: 60 components each contributing styles to . // Stresses head deduplication O(N^2) with JSON.stringify. 'pages/many-head-elements.astro': `\ --- import StyledWidget from '../components/StyledWidget.astro'; import UniqueStyled from '../components/UniqueStyled.astro'; // 30 instances of same component (dedup should collapse these) const duplicated = Array.from({ length: 30 }, (_, i) => i); // 30 instances with unique styles (dedup must compare all) const unique = Array.from({ length: 30 }, (_, i) => i); --- Many Head Elements

Head Deduplication Stress Test

{duplicated.map((i) => ( ))} {unique.map((i) => ( ))} `, // #9: Components with multiple named slots, only default used. // Stresses eager slot prerendering of unused slots. 'pages/many-slots.astro': `\ --- import Layout from '../components/Layout.astro'; import Section from '../components/Section.astro'; const sections = Array.from({ length: 100 }, (_, i) => ({ heading: \`Section \${i}\`, content: \`Content for section \${i} with some text to render.\`, })); ---

100 Sections with Slots

{sections.map((s) => (

{s.content}

))}
`, // #8: Large array .map() with component children. // Stresses BufferedRenderer-per-array-child allocation. 'pages/large-array.astro': `\ --- import Card from '../components/Card.astro'; const items = Array.from({ length: 5000 }, (_, i) => ({ title: \`Item \${i}\`, href: \`/item/\${i}\`, index: i, })); --- Large Array

5000 Array Items with Components

{items.map((item) => ( ))}
`, // #1, #11, #12: Mostly static HTML with very few expressions. // Baseline for measuring overhead of the rendering machinery on static content. 'pages/static-heavy.astro': `\ --- const title = "Static Heavy Page"; --- {title}

{title}

${Array.from({ length: 200 }) .map( (_, i) => `

Section ${i}

${loremIpsumHtml}

${loremIpsumHtml}

`, ) .join('\n ')} `, }; // --------------------------------------------------------------------------- // Exports // --------------------------------------------------------------------------- export const renderPages = Object.keys(pages) .filter((f) => f.startsWith('pages/')) .map((f) => f.replace('pages/', '')); /** * @param {URL} projectDir */ export async function run(projectDir) { await fs.rm(projectDir, { recursive: true, force: true }); await fs.mkdir(new URL('./src/pages', projectDir), { recursive: true }); await fs.mkdir(new URL('./src/components', projectDir), { recursive: true }); const allFiles = { ...components, ...pages }; await Promise.all( Object.entries(allFiles).map(([name, content]) => { return fs.writeFile(new URL(`./src/${name}`, projectDir), content, 'utf-8'); }), ); await fs.writeFile( new URL('./astro.config.js', projectDir), `\ import { defineConfig } from 'astro/config'; import adapter from '@benchmark/adapter'; export default defineConfig({ output: 'server', adapter: adapter(), });`, 'utf-8', ); }