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
18 changes: 18 additions & 0 deletions packages/docs/src/components/MPACharts.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
import ChartTabs from './ChartTabs.astro'
import MPAFPChart from './MPAFPChart.astro'
import MPAFCPChart from './MPAFCPChart.astro'
import MPAINPChart from './MPAINPChart.astro'
---

<ChartTabs
sectionId="mpa"
label="MPA performance graphs"
tab1Label="First Paint"
tab2Label="First Contentful Paint"
tab3Label="Interaction to Next Paint"
>
<MPAFPChart slot="panel-1" />
<MPAFCPChart slot="panel-2" />
<MPAINPChart slot="panel-3" />
</ChartTabs>
10 changes: 10 additions & 0 deletions packages/docs/src/components/MPAFCPChart.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import { chartMPAFCPData } from '../lib/collections'
import ComparisonBarChart from './ComparisonBarChart.astro'
---

<ComparisonBarChart
title="First Contentful Paint (ms)"
data={chartMPAFCPData}
valueFormat="ms"
/>
10 changes: 10 additions & 0 deletions packages/docs/src/components/MPAFPChart.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import { chartMPAFPData } from '../lib/collections'
import ComparisonBarChart from './ComparisonBarChart.astro'
---

<ComparisonBarChart
title="First Paint (ms)"
data={chartMPAFPData}
valueFormat="ms"
/>
10 changes: 10 additions & 0 deletions packages/docs/src/components/MPAINPChart.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import { chartMPAINPData } from '../lib/collections'
import ComparisonBarChart from './ComparisonBarChart.astro'
---

<ComparisonBarChart
title="Interaction to Next Paint (ms)"
data={chartMPAINPData}
valueFormat="ms"
/>
28 changes: 28 additions & 0 deletions packages/docs/src/components/MPAStatsMethodologyNotes.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
import MethodologyNotes from '../components/MethodologyNotes.astro'
---

<MethodologyNotes>
<li>Each framework renders a table of 1000 rows with two UUID columns</li>
<li>
Measured using Lighthouse flow with Chromium via Puppeteer for accurate
browser metrics
</li>
<li>
First Paint and First Contentful Paint are measured on initial navigation
</li>
<li>
Interaction to Next Paint is measured by clicking the first row's detail
link
</li>
<li>Benchmarks run 5 times and results are averaged</li>
<li>
Next.js, TanStack Start, and React Router default to SSR with no per-route
opt-out. Next.js wraps the SPA table in a <code>dynamic</code> import with <code
>ssr: false</code
> to prevent build-time prerendering. TanStack Start uses its built-in spa mode.
React Router disables SSR entirely via <code>ssr: false</code> in its config.
All other frameworks (Nuxt, SvelteKit, SolidStart, Astro) disable SSR per-route
without a separate build.
</li>
</MethodologyNotes>
33 changes: 33 additions & 0 deletions packages/docs/src/components/MPAStatsTable.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import { mpaStats } from '../lib/collections'
import { getFrameworkSlug } from '../lib/utils'
import MethodologyTag from './MethodologyTag.astro'
import StatsTable from './StatsTable.astro'

const columns = [
{
key: 'name',
header: 'Framework',
nameCell: true,
href: (row: Record<string, unknown>) =>
row.package !== 'app-baseline-html'
? `/framework/${getFrameworkSlug(row.package as string)}`
: null,
},
{ key: 'mpaFirstPaintMs', header: 'First Paint' },
{ key: 'mpaFCPMs', header: 'FCP' },
{ key: 'mpaINPMs', header: 'INP' },
]

const tableData = mpaStats
---

<MethodologyTag>
Measured on GitHub Actions (ubuntu-latest, Node 24) using Lighthouse flow with
Chromium.
</MethodologyTag>
<StatsTable
label="MPA performance by framework"
columns={columns}
data={tableData}
/>
5 changes: 5 additions & 0 deletions packages/docs/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const runtimeCollection = defineCollection({
spaFCPMs: z.number().optional(),
spaINPMs: z.number().optional(),
spaRuns: z.number().optional(),
// MPA paint + interaction metrics
mpaFirstPaintMs: z.number().optional(),
mpaFCPMs: z.number().optional(),
mpaINPMs: z.number().optional(),
mpaRuns: z.number().optional(),
}),
})

Expand Down
10 changes: 7 additions & 3 deletions packages/docs/src/content/runtime/app-astro.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
"ssrSamples": 3656,
"ssrBodySizeKb": 99.86,
"ssrDuplicationFactor": 1,
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:06:35.326Z",
"runner": "local",
"frameworkVersion": "5.16.15",
"order": 1,
"spaFirstPaintMs": 36.8,
"spaFCPMs": 36.8,
"spaINPMs": 98.72,
"spaRuns": 5
"spaRuns": 5,
"mpaFirstPaintMs": 90.33,
"mpaFCPMs": 90.39,
"mpaINPMs": 22.56,
"mpaRuns": 3
}
16 changes: 10 additions & 6 deletions packages/docs/src/content/runtime/app-next-js.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
"package": "app-next-js",
"isFocused": true,
"type": "ssr-app",
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:07:39.298Z",
"runner": "local",
"frameworkVersion": "16.1.1",
"ssrOpsPerSec": 129,
"ssrAvgLatencyMs": 7.74,
"ssrSamples": 1293,
"ssrBodySizeKb": 198.59,
"ssrDuplicationFactor": 2,
"order": 3,
"spaFirstPaintMs": 48,
"spaFCPMs": 48,
"spaINPMs": 75.48,
"spaRuns": 5
"spaFirstPaintMs": 371,
"spaFCPMs": 370.94,
"spaINPMs": 23.38,
"spaRuns": 1,
"mpaFirstPaintMs": 127.33,
"mpaFCPMs": 127.19,
"mpaINPMs": 20.21,
"mpaRuns": 3
}
16 changes: 10 additions & 6 deletions packages/docs/src/content/runtime/app-nuxt.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
"ssrSamples": 2478,
"ssrBodySizeKb": 201.18,
"ssrDuplicationFactor": 2,
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:26:28.141Z",
"runner": "local",
"frameworkVersion": "4.2.2",
"order": 4,
"spaFirstPaintMs": 32.8,
"spaFCPMs": 32.8,
"spaINPMs": 69.98,
"spaRuns": 5
"mpaFirstPaintMs": 89.67,
"mpaFCPMs": 89.6,
"mpaINPMs": 24.05,
"mpaRuns": 3,
"spaFirstPaintMs": 111.67,
"spaFCPMs": 111.7,
"spaINPMs": 22.65,
"spaRuns": 3
}
16 changes: 10 additions & 6 deletions packages/docs/src/content/runtime/app-react-router.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
"package": "app-react-router",
"isFocused": true,
"type": "ssr-app",
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:29:46.068Z",
"runner": "local",
"frameworkVersion": "7.10.1",
"ssrOpsPerSec": 64,
"ssrAvgLatencyMs": 15.528,
"ssrSamples": 644,
"ssrBodySizeKb": 211.14,
"ssrDuplicationFactor": 2,
"order": 5,
"spaFirstPaintMs": 35.2,
"spaFCPMs": 35.2,
"spaINPMs": 59.78,
"spaRuns": 5
"mpaFirstPaintMs": 167.33,
"mpaFCPMs": 167.24,
"mpaINPMs": 24.37,
"mpaRuns": 3,
"spaFirstPaintMs": 121,
"spaFCPMs": 121.16,
"spaINPMs": 22.24,
"spaRuns": 3
}
16 changes: 10 additions & 6 deletions packages/docs/src/content/runtime/app-solid-start.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
"package": "app-solid-start",
"isFocused": true,
"type": "ssr-app",
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:30:50.803Z",
"runner": "local",
"frameworkVersion": "1.2.1",
"ssrOpsPerSec": 234,
"ssrAvgLatencyMs": 4.275,
"ssrSamples": 2340,
"ssrBodySizeKb": 225.49,
"ssrDuplicationFactor": 2,
"order": 6,
"spaFirstPaintMs": 22.4,
"spaFCPMs": 22.4,
"spaINPMs": 63.09,
"spaRuns": 5
"mpaFirstPaintMs": 106.33,
"mpaFCPMs": 106.31,
"mpaINPMs": 23.79,
"mpaRuns": 3,
"spaFirstPaintMs": 114,
"spaFCPMs": 114.32,
"spaINPMs": 21.33,
"spaRuns": 3
}
16 changes: 10 additions & 6 deletions packages/docs/src/content/runtime/app-sveltekit.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
"package": "app-sveltekit",
"isFocused": true,
"type": "ssr-app",
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:11:57.395Z",
"runner": "local",
"frameworkVersion": "2.49.4",
"ssrOpsPerSec": 259,
"ssrAvgLatencyMs": 3.858,
"ssrSamples": 2592,
"ssrBodySizeKb": 183.55,
"ssrDuplicationFactor": 2,
"order": 7,
"spaFirstPaintMs": 26.4,
"spaFCPMs": 26.4,
"spaINPMs": 64.89,
"spaRuns": 5
"spaFirstPaintMs": 93,
"spaFCPMs": 93.14,
"spaINPMs": 20.37,
"spaRuns": 1,
"mpaFirstPaintMs": 109,
"mpaFCPMs": 108.78,
"mpaINPMs": 21.66,
"mpaRuns": 3
}
16 changes: 10 additions & 6 deletions packages/docs/src/content/runtime/app-tanstack-start-react.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
"package": "app-tanstack-start-react",
"isFocused": true,
"type": "ssr-app",
"timingMeasuredAt": "2026-03-08T00:31:57.171Z",
"runner": "ubuntu-latest",
"timingMeasuredAt": "2026-04-21T05:31:57.912Z",
"runner": "local",
"frameworkVersion": "1.145.3",
"ssrOpsPerSec": 185,
"ssrAvgLatencyMs": 5.395,
"ssrSamples": 1854,
"ssrBodySizeKb": 193.53,
"ssrDuplicationFactor": 2,
"order": 8,
"spaFirstPaintMs": 40.8,
"spaFCPMs": 40.8,
"spaINPMs": 59.27,
"spaRuns": 5
"mpaFirstPaintMs": 109.33,
"mpaFCPMs": 109.36,
"mpaINPMs": 23.98,
"mpaRuns": 3,
"spaFirstPaintMs": 693,
"spaFCPMs": 693.07,
"spaINPMs": 104.6,
"spaRuns": 3
}
37 changes: 36 additions & 1 deletion packages/docs/src/lib/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ const ssrStats = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)

const mpaStats = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)
.filter((f) => f?.name != null && Number.isFinite(f.mpaFirstPaintMs))
.map((f) => ({
name: f.name,
package: f.package,
isFocused: f.isFocused,
mpaFirstPaintMs: `${f.mpaFirstPaintMs}ms`,
mpaFCPMs: `${f.mpaFCPMs}ms`,
mpaINPMs: `${f.mpaINPMs}ms`,
}))

const spaStats = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)
Expand Down Expand Up @@ -63,6 +76,28 @@ export const chartDuplicateDependencyData = starterStats
focused: f.isFocused,
}))

export const chartMPAFPData = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)
.filter((f) => f?.name != null && Number.isFinite(f.mpaFirstPaintMs))
.map((f) => ({
name: f.name,
value: f.mpaFirstPaintMs!,
focused: f.isFocused,
}))

export const chartMPAFCPData = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)
.filter((f) => f?.name != null && Number.isFinite(f.mpaFCPMs))
.map((f) => ({ name: f.name, value: f.mpaFCPMs!, focused: f.isFocused }))

export const chartMPAINPData = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)
.filter((f) => f?.name != null && Number.isFinite(f.mpaINPMs))
.map((f) => ({ name: f.name, value: f.mpaINPMs!, focused: f.isFocused }))

export const chartSPAFPData = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)
Expand Down Expand Up @@ -100,4 +135,4 @@ export const coreJsTableData = starterStats.map((f) => {
}
})

export { ssrStats, spaStats, depsStats, buildInstallData }
export { ssrStats, spaStats, mpaStats, depsStats, buildInstallData }
Loading
Loading