Skip to content

Commit 0bdea67

Browse files
bundle-analyzer: add table view to compare mode (#93522)
Adds a table view to compare mode, switchable from the treemap via a toggle in the top bar. The `DiffTable` lists every source file with its A/B sizes, delta, and env badges, and supports text search. Removed sources (which can't appear in the treemap since they don't exist in the current build) are only visible here. The view toggle defaults to Table when entering compare mode and resets to Treemap when leaving it. <!-- 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>
1 parent 2f3f74b commit 0bdea67

4 files changed

Lines changed: 1083 additions & 23 deletions

File tree

‎apps/bundle-analyzer/app/page.tsx‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use client'
22

3-
import { useEffect, useMemo, useState, type ReactNode } from 'react'
3+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
44
import useSWR from 'swr'
55
import { CompareLayout } from '@/components/compare-layout'
66
import { ErrorState } from '@/components/error-state'
77
import { Sidebar } from '@/components/sidebar'
8-
import { TopBar, Environment } from '@/components/top-bar'
8+
import { TopBar, Environment, CompareView } from '@/components/top-bar'
99
import { TreemapVisualizer } from '@/components/treemap-visualizer'
1010

1111
import { Badge } from '@/components/ui/badge'
@@ -58,6 +58,22 @@ export default function Home() {
5858
const comparisonBaseDir = comparisonSnapshot
5959
? `history/${comparisonSnapshot.id}`
6060
: 'data'
61+
// Default view depends on mode: compare mode opens to the diff table
62+
// (the change list is the headline), single-build mode opens to the
63+
// treemap (size-by-area is the headline). Whenever the user toggles
64+
// between modes we reset to that mode's default; explicit user choices
65+
// within a mode are preserved until the mode changes again.
66+
const [compareView, setCompareView] = useState<CompareView>(
67+
CompareView.Treemap
68+
)
69+
const wasCompareModeRef = useRef(baselineSnapshot != null)
70+
useEffect(() => {
71+
const isNowCompare = baselineSnapshot != null
72+
if (wasCompareModeRef.current !== isNowCompare) {
73+
wasCompareModeRef.current = isNowCompare
74+
setCompareView(isNowCompare ? CompareView.Table : CompareView.Treemap)
75+
}
76+
}, [baselineSnapshot])
6177

6278
const {
6379
data: modulesData,
@@ -101,10 +117,8 @@ export default function Home() {
101117
{ revalidateOnFocus: false, revalidateOnReconnect: false }
102118
)
103119

104-
// Whether the selected route exists on comparison side B. We only fetch
105-
// its analyze.data when this is true because an output directory can contain
106-
// stale per-route subdirectories from previous builds. routes.json is the
107-
// source of truth for either a live or historical comparison build.
120+
// Whether the selected route exists on comparison side B. routes.json is
121+
// the source of truth because output directories can retain stale routes.
108122
const currentAnalyzeRouteExists =
109123
currentRoutes != null &&
110124
selectedRoute != null &&
@@ -336,6 +350,8 @@ export default function Home() {
336350
<CompareLayout
337351
baselineSnapshot={baselineSnapshot}
338352
comparisonSnapshot={comparisonSnapshot}
353+
compareView={compareView}
354+
searchQuery={searchQuery}
339355
selectedRoute={selectedRoute}
340356
comparisonRouteCount={currentRoutes?.length ?? null}
341357
routeDiff={routeDiff}
@@ -424,6 +440,9 @@ export default function Home() {
424440
<main className="h-screen flex flex-col bg-background">
425441
<TopBar
426442
hasSourceData={analyzeData != null}
443+
showViewToggle={isCompareMode}
444+
compareView={compareView}
445+
onCompareViewChange={setCompareView}
427446
selectedRoute={selectedRoute}
428447
setSelectedRoute={setSelectedRoute}
429448
environmentFilter={environmentFilter}

‎apps/bundle-analyzer/components/compare-layout.tsx‎

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import { useSidebarResize } from '@/lib/use-sidebar-resize'
44
import { CompareSidebar } from '@/components/sidebar'
5+
import { DiffTable } from '@/components/diff-table'
56
import { DiffTreemap } from '@/components/diff-treemap'
67
import { TreemapSkeleton } from '@/components/ui/skeleton'
78
import { StatCard, CountCard } from '@/components/stat-cards'
8-
import { Environment } from '@/components/top-bar'
9+
import { CompareView, Environment } from '@/components/top-bar'
910
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
1011
import {
1112
diffRoutesWithSizes,
@@ -19,6 +20,7 @@ import { cn, formatBytes } from '@/lib/utils'
1920
export interface CompareLayoutProps {
2021
baselineSnapshot: SnapshotMetadata
2122
comparisonSnapshot: SnapshotMetadata | null
23+
compareView: CompareView
2224
selectedRoute: string | null
2325
comparisonRouteCount: number | null
2426
routeDiff: ReturnType<typeof diffRoutesWithSizes> | null
@@ -29,6 +31,7 @@ export interface CompareLayoutProps {
2931
isBaselineAnalyzeLoading: boolean
3032
baselineAnalyzeError: unknown | null
3133
compressed: boolean
34+
searchQuery: string
3235
compareSelectedKey: string | null
3336
setCompareSelectedKey: (key: string | null) => void
3437
modulesData: ModulesData | null
@@ -39,17 +42,17 @@ export interface CompareLayoutProps {
3942
}
4043

4144
/**
42-
* Renders the comparison view (treemap) with a context strip at the top
43-
* showing the two builds being compared. The "before" build (A) is rendered
44-
* on the left and the selected "after" build (B) on the right,
45-
* matching the user's mental model.
45+
* Renders the comparison view (treemap + table) with a context strip at the
46+
* top showing the two builds being compared. The "before" build (A) is
47+
* rendered on the left and the selected "after" build (B) on the right, matching the user's mental model.
4648
*
4749
* Route selection lives in the top-bar route picker (`RouteTypeahead`),
4850
* which also renders per-route deltas in compare mode.
4951
*/
5052
export function CompareLayout({
5153
baselineSnapshot,
5254
comparisonSnapshot,
55+
compareView,
5356
selectedRoute,
5457
comparisonRouteCount,
5558
routeDiff,
@@ -60,6 +63,7 @@ export function CompareLayout({
6063
isBaselineAnalyzeLoading,
6164
baselineAnalyzeError,
6265
compressed,
66+
searchQuery,
6367
compareSelectedKey,
6468
setCompareSelectedKey,
6569
modulesData,
@@ -100,19 +104,23 @@ export function CompareLayout({
100104
</div>
101105

102106
{/*
103-
Per-route source diff + sidebar. The sidebar slides in when a
107+
Per-route source diff + sidebar. The sidebar slides in when a row or
104108
treemap tile is selected and shows the import chain for that source.
105109
*/}
106110
<div className="flex flex-1 min-h-0">
107111
<div className="flex flex-1 min-w-0 flex-col">
108112
<ComparePerRoutePanel
113+
compareView={compareView}
109114
selectedRoute={selectedRoute}
110115
sourceDiff={sourceDiff}
111116
analyzeData={analyzeData}
112117
baselineAnalyzeData={baselineAnalyzeData}
113118
isAnalyzeLoading={isAnalyzeLoading}
114119
isBaselineAnalyzeLoading={isBaselineAnalyzeLoading}
115120
compressed={compressed}
121+
searchQuery={searchQuery}
122+
baselineSnapshot={baselineSnapshot}
123+
comparisonSnapshot={comparisonSnapshot}
116124
compareSelectedKey={compareSelectedKey}
117125
onCompareSelectedKeyChange={setCompareSelectedKey}
118126
/>
@@ -370,28 +378,36 @@ function RouteStatsBody({
370378
}
371379

372380
/**
373-
* Per-route comparison panel. Shows the diff treemap. Handles missing-route
374-
* states (the route is new in the comparison build, or was removed) with
375-
* friendly messaging.
381+
* Per-route comparison panel. Shows either the diff treemap or the diff table,
382+
* controlled by `compareView`. Handles missing-route states (the route is new
383+
* in comparison build B, or was removed) with friendly messaging.
376384
*/
377385
function ComparePerRoutePanel({
386+
compareView,
378387
selectedRoute,
379388
sourceDiff,
380389
analyzeData,
381390
baselineAnalyzeData,
382391
isAnalyzeLoading,
383392
isBaselineAnalyzeLoading,
384393
compressed,
394+
searchQuery,
395+
baselineSnapshot,
396+
comparisonSnapshot,
385397
compareSelectedKey,
386398
onCompareSelectedKeyChange,
387399
}: {
400+
compareView: CompareView
388401
selectedRoute: string | null
389402
sourceDiff: DiffSummary<SourceDiffRow> | null
390403
analyzeData: AnalyzeData | null
391404
baselineAnalyzeData: AnalyzeData | null
392405
isAnalyzeLoading: boolean
393406
isBaselineAnalyzeLoading: boolean
394407
compressed: boolean
408+
searchQuery: string
409+
baselineSnapshot: SnapshotMetadata
410+
comparisonSnapshot: SnapshotMetadata | null
395411
compareSelectedKey: string | null
396412
onCompareSelectedKeyChange: (key: string | null) => void
397413
}) {
@@ -419,14 +435,31 @@ function ComparePerRoutePanel({
419435

420436
return (
421437
<div className="flex flex-1 min-h-0 flex-col">
422-
<DiffTreemap
423-
summary={sourceDiff}
424-
useCompressed={compressed}
425-
analyzeData={analyzeData}
426-
baselineAnalyzeData={baselineAnalyzeData}
427-
selectedKey={compareSelectedKey}
428-
onSelectKey={onCompareSelectedKeyChange}
429-
/>
438+
{compareView === CompareView.Treemap ? (
439+
<DiffTreemap
440+
summary={sourceDiff}
441+
useCompressed={compressed}
442+
analyzeData={analyzeData}
443+
baselineAnalyzeData={baselineAnalyzeData}
444+
selectedKey={compareSelectedKey}
445+
onSelectKey={onCompareSelectedKeyChange}
446+
/>
447+
) : (
448+
<DiffTable
449+
summary={sourceDiff}
450+
useCompressed={compressed}
451+
nameHeading="Source"
452+
aHeading={formatSnapshotLabel(baselineSnapshot)}
453+
bHeading={
454+
comparisonSnapshot
455+
? formatSnapshotLabel(comparisonSnapshot)
456+
: 'Latest'
457+
}
458+
searchQuery={searchQuery}
459+
selectedKey={compareSelectedKey}
460+
onRowSelect={(row) => onCompareSelectedKeyChange(row.key)}
461+
/>
462+
)}
430463
</div>
431464
)
432465
}

0 commit comments

Comments
 (0)