Skip to content

Commit 585126e

Browse files
authored
bundle-analyzer: Add sizes to treemap, status bar (#86966)
![CleanShot 2025-12-08 at 19.58.55@2x.png](https://app.graphite.com/user-attachments/assets/43f13844-abe2-4a1b-bbc4-764554ca6634.png)
1 parent 2ac04b0 commit 585126e

File tree

6 files changed

+113
-24
lines changed

6 files changed

+113
-24
lines changed

apps/bundle-analyzer/app/page.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
import { Sidebar } from '@/components/sidebar'
1212
import { TreemapVisualizer } from '@/components/treemap-visualizer'
1313

14+
import { Badge } from '@/components/ui/badge'
1415
import { Input } from '@/components/ui/input'
1516
import { TreemapSkeleton } from '@/components/ui/skeleton'
1617
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
1718
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
1819
import { computeActiveEntries, computeModuleDepthMap } from '@/lib/module-graph'
1920
import { fetchStrict } from '@/lib/utils'
21+
import { formatBytes } from '@/lib/utils'
2022

2123
export default function Home() {
2224
const [selectedRoute, setSelectedRoute] = useState<string | null>(null)
@@ -65,6 +67,7 @@ export default function Home() {
6567
const [isMouseInTreemap, setIsMouseInTreemap] = useState(false)
6668
const [hoveredNodeInfo, setHoveredNodeInfo] = useState<{
6769
name: string
70+
size: number
6871
server?: boolean
6972
client?: boolean
7073
} | null>(null)
@@ -284,30 +287,30 @@ export default function Home() {
284287

285288
{analyzeData && (
286289
<div className="flex-none border-t border-border bg-background px-4 py-2 h-10">
287-
<p className="text-sm text-muted-foreground">
290+
<div className="text-sm text-muted-foreground">
288291
{hoveredNodeInfo ? (
289292
<>
290293
<span className="font-medium text-foreground">
291294
{hoveredNodeInfo.name}
292295
</span>
296+
<span className="ml-2 text-muted-foreground">
297+
{formatBytes(hoveredNodeInfo.size)}
298+
</span>
293299
{(hoveredNodeInfo.server || hoveredNodeInfo.client) && (
294-
<span className="ml-2 text-xs">
300+
<span className="ml-2 inline-flex gap-1">
295301
{hoveredNodeInfo.client && (
296-
<span className="text-primary">[client]</span>
297-
)}
298-
{hoveredNodeInfo.server && hoveredNodeInfo.client && (
299-
<span> </span>
302+
<Badge variant="client">client</Badge>
300303
)}
301304
{hoveredNodeInfo.server && (
302-
<span className="text-primary">[server]</span>
305+
<Badge variant="server">server</Badge>
303306
)}
304307
</span>
305308
)}
306309
</>
307310
) : (
308311
'Hover over a file to see details'
309312
)}
310-
</p>
313+
</div>
311314
</div>
312315
)}
313316
</main>

apps/bundle-analyzer/components/sidebar.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Skeleton } from '@/components/ui/skeleton'
66
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
77
import { SpecialModule } from '@/lib/types'
88
import { getSpecialModuleType } from '@/lib/utils'
9+
import { Badge } from './ui/badge'
910

1011
interface SidebarProps {
1112
sidebarWidth: number
@@ -121,14 +122,16 @@ export function Sidebar({
121122
analyzeData.source(selectedSourceIndex) &&
122123
(specialModuleType === SpecialModule.POLYFILL_MODULE ||
123124
specialModuleType === SpecialModule.POLYFILL_NOMODULE) && (
124-
<div className="flex items-center gap-2">
125-
<dt className="inline-flex items-center rounded-md bg-polyfill/10 dark:bg-polyfill/30 px-2 py-1 text-xs font-medium text-polyfill dark:text-polyfill-foreground ring-1 ring-inset ring-polyfill/20 shrink-0">
126-
Polyfill
127-
</dt>
128-
<dd className="text-xs text-muted-foreground">
129-
Next.js built-in polyfills
130-
</dd>
131-
</div>
125+
<dl>
126+
<div className="flex items-center gap-2">
127+
<dt className="inline-flex items-center">
128+
<Badge variant="polyfill">Polyfill</Badge>
129+
</dt>
130+
<dd className="text-xs text-muted-foreground">
131+
Next.js built-in polyfills
132+
</dd>
133+
</div>
134+
</dl>
132135
)}
133136

134137
{selectedSourceIndex != null &&

apps/bundle-analyzer/components/treemap-visualizer.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type LayoutNodeInfo,
1111
} from '@/lib/treemap-layout'
1212
import { SpecialModule } from '@/lib/types'
13+
import { formatBytes } from '@/lib/utils'
1314

1415
interface TreemapVisualizerProps {
1516
analyzeData: AnalyzeData
@@ -337,15 +338,24 @@ function drawTreemap(
337338
if (rect.width > 60 && rect.height > 30) {
338339
const textColor = readableColor(color)
339340
ctx.fillStyle = textColor
340-
ctx.font = '12px sans-serif'
341341
ctx.textAlign = 'center'
342342
ctx.textBaseline = 'middle'
343343

344344
const maxWidth = rect.width - 8
345345
let displayName = name
346-
const metrics = ctx.measureText(displayName)
347346

348-
if (metrics.width > maxWidth) {
347+
const sizeText = formatBytes(node.size)
348+
const fontSize = 12
349+
const sizeFontSize = 10
350+
const lineHeight = fontSize + 2
351+
352+
// Check if we have space for both name and size
353+
const hasSpaceForSize = rect.height > 50
354+
355+
ctx.font = `${fontSize}px sans-serif`
356+
const nameMetrics = ctx.measureText(displayName)
357+
358+
if (nameMetrics.width > maxWidth) {
349359
while (
350360
displayName.length > 0 &&
351361
ctx.measureText(`${displayName}...`).width > maxWidth
@@ -355,11 +365,31 @@ function drawTreemap(
355365
displayName += '...'
356366
}
357367

358-
ctx.fillText(
359-
displayName,
360-
rect.x + rect.width / 2,
361-
rect.y + rect.height / 2
362-
)
368+
if (hasSpaceForSize) {
369+
ctx.font = `${fontSize}px sans-serif`
370+
ctx.fillText(
371+
displayName,
372+
rect.x + rect.width / 2,
373+
rect.y + rect.height / 2 - lineHeight / 2
374+
)
375+
376+
ctx.globalAlpha = opacity * 0.75
377+
ctx.font = `${sizeFontSize}px sans-serif`
378+
ctx.fillText(
379+
sizeText,
380+
rect.x + rect.width / 2,
381+
rect.y + rect.height / 2 + lineHeight / 2
382+
)
383+
ctx.globalAlpha = opacity
384+
} else {
385+
// Only name fits, draw it centered
386+
ctx.font = `${fontSize}px sans-serif`
387+
ctx.fillText(
388+
displayName,
389+
rect.x + rect.width / 2,
390+
rect.y + rect.height / 2
391+
)
392+
}
363393
}
364394

365395
ctx.globalAlpha = 1.0
@@ -797,6 +827,7 @@ export function TreemapVisualizer({
797827
if (node) {
798828
const nodeInfo = {
799829
name: node.name,
830+
size: node.size,
800831
server: node.server,
801832
client: node.client,
802833
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { cva, type VariantProps } from 'class-variance-authority'
2+
import * as React from 'react'
3+
4+
import { cn } from '@/lib/utils'
5+
6+
const badgeVariants = cva(
7+
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8+
{
9+
variants: {
10+
variant: {
11+
default:
12+
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
13+
secondary:
14+
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15+
destructive:
16+
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
17+
outline: 'text-foreground',
18+
client:
19+
'border-transparent bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-300/20',
20+
server:
21+
'border-transparent bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-700/10 dark:ring-purple-300/20',
22+
polyfill:
23+
'border-transparent bg-polyfill/10 dark:bg-polyfill/30 text-polyfill dark:text-polyfill-foreground ring-1 ring-inset ring-polyfill/20',
24+
},
25+
},
26+
defaultVariants: {
27+
variant: 'default',
28+
},
29+
}
30+
)
31+
32+
export interface BadgeProps
33+
extends React.HTMLAttributes<HTMLSpanElement>,
34+
VariantProps<typeof badgeVariants> {}
35+
36+
function Badge({ className, variant, ...props }: BadgeProps) {
37+
return (
38+
<span className={cn(badgeVariants({ variant }), className)} {...props} />
39+
)
40+
}
41+
42+
export { Badge, badgeVariants }

apps/bundle-analyzer/lib/treemap-layout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface LayoutRect {
1212

1313
export interface LayoutNodeInfo {
1414
name: string
15+
size: number
1516
server?: boolean
1617
client?: boolean
1718
}

apps/bundle-analyzer/lib/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ export function getSpecialModuleType(
4343
return null
4444
}
4545

46+
export function formatBytes(bytes: number): string {
47+
if (bytes === 0) return '0 B'
48+
if (bytes < 1024) return `${bytes} B`
49+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
50+
if (bytes < 1024 * 1024 * 1024)
51+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
52+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
53+
}
54+
4655
let IDENT_ATTRIBUTES_REGEXP =
4756
/^(.+?)(?: \{(.*)\})?(?: \[(.*)\])?(?: \((.*?)\))?(?: <(.*?)>)?$/
4857

0 commit comments

Comments
 (0)