Files
vercel__next.js/apps/bundle-analyzer/lib/use-route-totals.ts
Will Binns-Smith 2f3f74b13f bundle-analyzer: add compare view with treemap diff (#93521)
Adds a compare mode to the bundle analyzer. When the user picks a
historical snapshot from the new baseline picker in the top bar, the UI
switches into compare mode: routes are diff'd by name and size, and the
per-route view shows a `DiffTreemap` that colors each source tile by its
change status (added/removed/grew/shrank/unchanged).

The compare sidebar shows the import chain for the selected tile on
either the baseline ("A") or current ("B") side. Route totals are
fetched in parallel so the route picker can sort by largest impact and
classify routes as `changed`/`identical` rather than just name-presence.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Will Binns-Smith <755844+wbinnssmith@users.noreply.github.com>
2026-09-10 12:47:55 -07:00

82 lines
2.5 KiB
TypeScript

import { useEffect, useState } from 'react'
import { AnalyzeData } from './analyze-data'
import { totalsFromAnalyzeData, type RouteSizeTotals } from './diff'
import { fetchStrict } from './utils'
/**
* Resolve the URL of an `analyze.data` file for a given route, parameterised
* by base directory (`'data'` for the live build, `'history/<id>'` for a
* historical snapshot).
*/
function analyzeDataUrl(route: string, baseDir: string): string {
if (route === '/') return `${baseDir}/analyze.data`
return `${baseDir}/${route.replace(/^\//, '')}/analyze.data`
}
/**
* Result of loading route totals for one side. `totals` is keyed by route.
* Routes whose `analyze.data` failed to load (e.g. deleted between builds)
* are simply absent from the map; callers should treat absence as
* "not present in this build".
*/
export interface RouteTotalsState {
totals: ReadonlyMap<string, RouteSizeTotals> | null
isLoading: boolean
}
/**
* Loads `analyze.data` for every route in `routes` from `baseDir` and
* reduces each to its size totals. Used to power the route-level diff so
* that routes can be classified `changed`/`identical` by real bundle size,
* not just name presence.
*
* The fetch fan-out is parallel; for projects with many routes this can be
* heavy, but typical analyze runs have a small number of routes. Results
* are stored in module-scoped state keyed by `baseDir` to avoid refetching
* across re-renders.
*/
export function useRouteTotals(
routes: string[] | null | undefined,
baseDir: string | null
): RouteTotalsState {
const [state, setState] = useState<RouteTotalsState>({
totals: null,
isLoading: false,
})
useEffect(() => {
if (!routes || !baseDir) {
setState({ totals: null, isLoading: false })
return
}
let cancelled = false
setState((prev) => ({ totals: prev.totals, isLoading: true }))
Promise.all(
routes.map(async (route) => {
try {
const resp = await fetchStrict(analyzeDataUrl(route, baseDir))
const data = new AnalyzeData(await resp.arrayBuffer())
return [route, totalsFromAnalyzeData(data)] as const
} catch {
return null
}
})
).then((results) => {
if (cancelled) return
const totals = new Map<string, RouteSizeTotals>()
for (const result of results) {
if (result) totals.set(result[0], result[1])
}
setState({ totals, isLoading: false })
})
return () => {
cancelled = true
}
}, [routes, baseDir])
return state
}