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
28 changes: 28 additions & 0 deletions packages/docs/src/components/SPAStatsMethodologyNotes.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/SPAStatsTable.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import { spaStats } 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: 'spaFirstPaintMs', header: 'First Paint' },
{ key: 'spaFCPMs', header: 'FCP' },
{ key: 'spaINPMs', header: 'INP' },
]

const tableData = spaStats
---

<MethodologyTag>
Measured on GitHub Actions (ubuntu-latest, Node 24) using Lighthouse flow with
Chromium.
</MethodologyTag>
<StatsTable
label="SPA performance by framework"
columns={columns}
data={tableData}
/>
17 changes: 15 additions & 2 deletions packages/docs/src/lib/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getCollection } from 'astro:content'
import { formatBytesToMB, formatTimeMs } from './utils'

const devtimeEntries = await getCollection('devtime')
const runtimeEntries = await getCollection('runtime')
export const runtimeEntries = await getCollection('runtime')

export const starterStats = devtimeEntries
.map((entry) => entry.data)
Expand All @@ -12,6 +12,19 @@ const ssrStats = runtimeEntries
.map((entry) => entry.data)
.sort((a, b) => a.order - b.order)

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

const depsStats = starterStats.map((f) => ({
name: f.name,
package: f.package,
Expand Down Expand Up @@ -87,4 +100,4 @@ export const coreJsTableData = starterStats.map((f) => {
}
})

export { ssrStats, depsStats, buildInstallData }
export { ssrStats, spaStats, depsStats, buildInstallData }
30 changes: 30 additions & 0 deletions packages/docs/src/pages/framework/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import BackLink from '../../components/BackLink.astro'
import DevTimeChart from '../../components/DevTimeChart.astro'
import MethodologyTag from '../../components/MethodologyTag.astro'
import PageHeader from '../../components/PageHeader.astro'
import SPAStatsMethodologyNotes from '../../components/SPAStatsMethodologyNotes.astro'
import SSRStatsMethodologyNotes from '../../components/SSRStatsMethodologyNotes.astro'
import StatsTable from '../../components/StatsTable.astro'
import Layout from '../../layouts/Layout.astro'
Expand Down Expand Up @@ -164,6 +165,24 @@ const ssrData = [
]
: []),
]

const spaColumns = [
{ key: 'name', header: 'Framework', nameCell: true },
{ key: 'spaFirstPaintMs', header: 'First Paint' },
{ key: 'spaFCPMs', header: 'FCP' },
{ key: 'spaINPMs', header: 'INP' },
]

const spaData = runtime
? [
{
name: runtime.name,
spaFirstPaintMs: `${runtime.spaFirstPaintMs}ms`,
spaFCPMs: `${runtime.spaFCPMs}ms`,
spaINPMs: `${runtime.spaINPMs}ms`,
},
]
: []
---

<Layout title={`${devtime.name} — Framework Tracker`}>
Expand Down Expand Up @@ -303,6 +322,17 @@ const ssrData = [
data={ssrData}
/>
<SSRStatsMethodologyNotes />
<h3>SPA Performance</h3>
<MethodologyTag>
Measured on GitHub Actions (ubuntu-latest, Node 24) using Lighthouse
flow with Chromium.
</MethodologyTag>
<StatsTable
label="SPA performance"
columns={spaColumns}
data={spaData}
/>
<SPAStatsMethodologyNotes />
</>
)
}
Expand Down
12 changes: 2 additions & 10 deletions packages/docs/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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'
Expand Down Expand Up @@ -58,20 +59,11 @@ import Layout from '../layouts/Layout.astro'
/>
<h2>Runtime Performance</h2>
<h3>SSR Performance</h3>
<SSRStatsTable />
<SSRCharts />
<DetailsLink href="/run-time#ssr-performance" label="SSR in Run Time Stats" />
<h3>SPA Performance</h3>
<SPACharts />
<p class="methodology-note">
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.
</p>
<DetailsLink href="/run-time#spa-performance" label="SPA in Run Time Stats" />
</Layout>

<style>
Expand Down
7 changes: 7 additions & 0 deletions packages/docs/src/pages/run-time.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
---
import FocusedToggle from '../components/FocusedToggle.astro'
import PageHeader from '../components/PageHeader.astro'
import SPACharts from '../components/SPACharts.astro'
import SPAStatsMethodologyNotes from '../components/SPAStatsMethodologyNotes.astro'
import SPAStatsTable from '../components/SPAStatsTable.astro'
import SSRStatsMethodologyNotes from '../components/SSRStatsMethodologyNotes.astro'
import SSRStatsTable from '../components/SSRStatsTable.astro'
import Layout from '../layouts/Layout.astro'
Expand All @@ -12,4 +15,8 @@ import Layout from '../layouts/Layout.astro'
<h2 id="ssr-performance">SSR Performance</h2>
<SSRStatsTable />
<SSRStatsMethodologyNotes />
<h2 id="spa-performance">SPA Performance</h2>
<SPACharts />
<SPAStatsTable />
<SPAStatsMethodologyNotes />
</Layout>
Loading