diff --git a/packages/docs/src/components/MPACharts.astro b/packages/docs/src/components/MPACharts.astro
new file mode 100644
index 00000000..fd43db18
--- /dev/null
+++ b/packages/docs/src/components/MPACharts.astro
@@ -0,0 +1,18 @@
+---
+import ChartTabs from './ChartTabs.astro'
+import MPAFPChart from './MPAFPChart.astro'
+import MPAFCPChart from './MPAFCPChart.astro'
+import MPAINPChart from './MPAINPChart.astro'
+---
+
+
+
+
+
+
diff --git a/packages/docs/src/components/MPAFCPChart.astro b/packages/docs/src/components/MPAFCPChart.astro
new file mode 100644
index 00000000..b0fd7d98
--- /dev/null
+++ b/packages/docs/src/components/MPAFCPChart.astro
@@ -0,0 +1,10 @@
+---
+import { chartMPAFCPData } from '../lib/collections'
+import ComparisonBarChart from './ComparisonBarChart.astro'
+---
+
+
diff --git a/packages/docs/src/components/MPAFPChart.astro b/packages/docs/src/components/MPAFPChart.astro
new file mode 100644
index 00000000..2d0861f9
--- /dev/null
+++ b/packages/docs/src/components/MPAFPChart.astro
@@ -0,0 +1,10 @@
+---
+import { chartMPAFPData } from '../lib/collections'
+import ComparisonBarChart from './ComparisonBarChart.astro'
+---
+
+
diff --git a/packages/docs/src/components/MPAINPChart.astro b/packages/docs/src/components/MPAINPChart.astro
new file mode 100644
index 00000000..f3f23c2b
--- /dev/null
+++ b/packages/docs/src/components/MPAINPChart.astro
@@ -0,0 +1,10 @@
+---
+import { chartMPAINPData } from '../lib/collections'
+import ComparisonBarChart from './ComparisonBarChart.astro'
+---
+
+
diff --git a/packages/docs/src/components/MPAStatsMethodologyNotes.astro b/packages/docs/src/components/MPAStatsMethodologyNotes.astro
new file mode 100644
index 00000000..4eaccdaf
--- /dev/null
+++ b/packages/docs/src/components/MPAStatsMethodologyNotes.astro
@@ -0,0 +1,28 @@
+---
+import MethodologyNotes from '../components/MethodologyNotes.astro'
+---
+
+
+ Each framework renders a table of 1000 rows with two UUID columns
+
+ Measured using Lighthouse flow with Chromium via Puppeteer for accurate
+ browser metrics
+
+
+ First Paint and First Contentful Paint are measured on initial navigation
+
+
+ Interaction to Next Paint is measured by clicking the first row's detail
+ link
+
+ Benchmarks run 5 times and results are averaged
+
+ Next.js, TanStack Start, and React Router default to SSR with no per-route
+ opt-out. Next.js wraps the SPA table in a dynamic import with ssr: false to prevent build-time prerendering. TanStack Start uses its built-in spa mode.
+ React Router disables SSR entirely via ssr: false in its config.
+ All other frameworks (Nuxt, SvelteKit, SolidStart, Astro) disable SSR per-route
+ without a separate build.
+
+
diff --git a/packages/docs/src/components/MPAStatsTable.astro b/packages/docs/src/components/MPAStatsTable.astro
new file mode 100644
index 00000000..2f4e9f5d
--- /dev/null
+++ b/packages/docs/src/components/MPAStatsTable.astro
@@ -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) =>
+ 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
+---
+
+
+ Measured on GitHub Actions (ubuntu-latest, Node 24) using Lighthouse flow with
+ Chromium.
+
+
diff --git a/packages/docs/src/content.config.ts b/packages/docs/src/content.config.ts
index ec52200c..55a5f22c 100644
--- a/packages/docs/src/content.config.ts
+++ b/packages/docs/src/content.config.ts
@@ -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(),
}),
})
diff --git a/packages/docs/src/content/runtime/app-astro.json b/packages/docs/src/content/runtime/app-astro.json
index d2c3aede..302c5cb1 100644
--- a/packages/docs/src/content/runtime/app-astro.json
+++ b/packages/docs/src/content/runtime/app-astro.json
@@ -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
}
diff --git a/packages/docs/src/content/runtime/app-next-js.json b/packages/docs/src/content/runtime/app-next-js.json
index 72d94746..4ad8f8b1 100644
--- a/packages/docs/src/content/runtime/app-next-js.json
+++ b/packages/docs/src/content/runtime/app-next-js.json
@@ -3,8 +3,8 @@
"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,
@@ -12,8 +12,12 @@
"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
}
diff --git a/packages/docs/src/content/runtime/app-nuxt.json b/packages/docs/src/content/runtime/app-nuxt.json
index d1307003..060d5190 100644
--- a/packages/docs/src/content/runtime/app-nuxt.json
+++ b/packages/docs/src/content/runtime/app-nuxt.json
@@ -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
}
diff --git a/packages/docs/src/content/runtime/app-react-router.json b/packages/docs/src/content/runtime/app-react-router.json
index 4deadbab..64f71996 100644
--- a/packages/docs/src/content/runtime/app-react-router.json
+++ b/packages/docs/src/content/runtime/app-react-router.json
@@ -3,8 +3,8 @@
"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,
@@ -12,8 +12,12 @@
"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
}
diff --git a/packages/docs/src/content/runtime/app-solid-start.json b/packages/docs/src/content/runtime/app-solid-start.json
index eeaa4f29..da53aae5 100644
--- a/packages/docs/src/content/runtime/app-solid-start.json
+++ b/packages/docs/src/content/runtime/app-solid-start.json
@@ -3,8 +3,8 @@
"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,
@@ -12,8 +12,12 @@
"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
}
diff --git a/packages/docs/src/content/runtime/app-sveltekit.json b/packages/docs/src/content/runtime/app-sveltekit.json
index bd164f45..1bdd079e 100644
--- a/packages/docs/src/content/runtime/app-sveltekit.json
+++ b/packages/docs/src/content/runtime/app-sveltekit.json
@@ -3,8 +3,8 @@
"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,
@@ -12,8 +12,12 @@
"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
}
diff --git a/packages/docs/src/content/runtime/app-tanstack-start-react.json b/packages/docs/src/content/runtime/app-tanstack-start-react.json
index a5ac7f38..6cc6e45c 100644
--- a/packages/docs/src/content/runtime/app-tanstack-start-react.json
+++ b/packages/docs/src/content/runtime/app-tanstack-start-react.json
@@ -3,8 +3,8 @@
"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,
@@ -12,8 +12,12 @@
"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
}
diff --git a/packages/docs/src/lib/collections.ts b/packages/docs/src/lib/collections.ts
index ad0cec22..ec51d68d 100644
--- a/packages/docs/src/lib/collections.ts
+++ b/packages/docs/src/lib/collections.ts
@@ -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)
@@ -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)
@@ -100,4 +135,4 @@ export const coreJsTableData = starterStats.map((f) => {
}
})
-export { ssrStats, spaStats, depsStats, buildInstallData }
+export { ssrStats, spaStats, mpaStats, depsStats, buildInstallData }
diff --git a/packages/docs/src/pages/framework/[slug].astro b/packages/docs/src/pages/framework/[slug].astro
index 0c8f23b0..31424cbc 100644
--- a/packages/docs/src/pages/framework/[slug].astro
+++ b/packages/docs/src/pages/framework/[slug].astro
@@ -3,6 +3,7 @@ import { getCollection } from 'astro:content'
import BackLink from '../../components/BackLink.astro'
import DevTimeChart from '../../components/DevTimeChart.astro'
import MethodologyTag from '../../components/MethodologyTag.astro'
+import MPAStatsMethodologyNotes from '../../components/MPAStatsMethodologyNotes.astro'
import PageHeader from '../../components/PageHeader.astro'
import SPAStatsMethodologyNotes from '../../components/SPAStatsMethodologyNotes.astro'
import SSRStatsMethodologyNotes from '../../components/SSRStatsMethodologyNotes.astro'
@@ -183,6 +184,24 @@ const spaData = runtime
},
]
: []
+
+const mpaColumns = [
+ { key: 'name', header: 'Framework', nameCell: true },
+ { key: 'mpaFirstPaintMs', header: 'First Paint' },
+ { key: 'mpaFCPMs', header: 'FCP' },
+ { key: 'mpaINPMs', header: 'INP' },
+]
+
+const mpaData = runtime
+ ? [
+ {
+ name: runtime.name,
+ mpaFirstPaintMs: `${runtime.mpaFirstPaintMs}ms`,
+ mpaFCPMs: `${runtime.mpaFCPMs}ms`,
+ mpaINPMs: `${runtime.mpaINPMs}ms`,
+ },
+ ]
+ : []
---
@@ -333,6 +352,17 @@ const spaData = runtime
data={spaData}
/>
+ MPA Performance
+
+ Measured on GitHub Actions (ubuntu-latest, Node 24) using Lighthouse
+ flow with Chromium.
+
+
+
>
)
}
diff --git a/packages/docs/src/pages/index.astro b/packages/docs/src/pages/index.astro
index 426b5425..d146fd0a 100644
--- a/packages/docs/src/pages/index.astro
+++ b/packages/docs/src/pages/index.astro
@@ -2,15 +2,14 @@
import BuildSizeCharts from '../components/BuildSizeCharts.astro'
import CoreJsTable from '../components/CoreJsTable.astro'
import DependencyCharts from '../components/DependencyCharts.astro'
+import MPACharts from '../components/MPACharts.astro'
import NodeModulesSizeCharts from '../components/NodeModulesSizeCharts.astro'
import Description from '../components/Description.astro'
import DetailsLink from '../components/DetailsLink.astro'
import FocusedToggle from '../components/FocusedToggle.astro'
import PageHeader from '../components/PageHeader.astro'
import SPACharts from '../components/SPACharts.astro'
-import SPAStatsTable from '../components/SPAStatsTable.astro'
import SSRCharts from '../components/SSRCharts.astro'
-import SSRStatsTable from '../components/SSRStatsTable.astro'
import Layout from '../layouts/Layout.astro'
---
@@ -64,6 +63,9 @@ import Layout from '../layouts/Layout.astro'
SPA Performance
+ MPA Performance
+
+