Skip to content

Commit b9af2e5

Browse files
committed
Show cache life column in build output
1 parent 00aba49 commit b9af2e5

File tree

5 files changed

+117
-61
lines changed

5 files changed

+117
-61
lines changed

packages/next/src/build/index.ts

+2-10
Original file line numberDiff line numberDiff line change
@@ -2905,23 +2905,15 @@ export default async function build(
29052905
...(pageInfos.get(route.pathname) as PageInfo),
29062906
hasPostponed,
29072907
hasEmptyPrelude,
2908-
// TODO: Enable the following line to show "ISR" status in build
2909-
// output. Requires different presentation to also work for app
2910-
// router routes.
2911-
// See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
2912-
// initialCacheControl: cacheControl,
2908+
initialCacheControl: cacheControl,
29132909
})
29142910

29152911
// update the page (eg /blog/[slug]) to also have the postpone metadata
29162912
pageInfos.set(page, {
29172913
...(pageInfos.get(page) as PageInfo),
29182914
hasPostponed,
29192915
hasEmptyPrelude,
2920-
// TODO: Enable the following line to show "ISR" status in build
2921-
// output. Requires different presentation to also work for app
2922-
// router routes.
2923-
// See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
2924-
// initialCacheControl: cacheControl,
2916+
initialCacheControl: cacheControl,
29252917
})
29262918

29272919
if (cacheControl.revalidate !== 0) {
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { CacheControl } from '../../server/lib/cache-control'
2+
3+
const timeUnits = [
4+
{ label: 'y', seconds: 31536000 },
5+
{ label: 'w', seconds: 604800 },
6+
{ label: 'd', seconds: 86400 },
7+
{ label: 'h', seconds: 3600 },
8+
{ label: 'min', seconds: 60 },
9+
{ label: 's', seconds: 1 },
10+
]
11+
12+
function humanReadableTimeRounded(seconds: number): string {
13+
// Find the largest fitting unit.
14+
let candidateIndex = timeUnits.length - 1
15+
for (let i = 0; i < timeUnits.length; i++) {
16+
if (seconds >= timeUnits[i].seconds) {
17+
candidateIndex = i
18+
break
19+
}
20+
}
21+
22+
const candidate = timeUnits[candidateIndex]
23+
const value = seconds / candidate.seconds
24+
const isExact = Number.isInteger(value)
25+
26+
// For days and weeks only, check if using the next smaller unit yields an
27+
// exact result.
28+
if (!isExact && (candidate.label === 'd' || candidate.label === 'w')) {
29+
const nextUnit = timeUnits[candidateIndex + 1]
30+
const nextValue = seconds / nextUnit.seconds
31+
32+
if (Number.isInteger(nextValue)) {
33+
return `${nextValue} ${nextUnit.label}`
34+
}
35+
}
36+
37+
if (isExact) {
38+
return `${value} ${candidate.label}`
39+
}
40+
41+
return `~${Math.round(value)} ${candidate.label}`
42+
}
43+
44+
export function formatCacheControl(cacheControl: CacheControl): string {
45+
const { revalidate, expire } = cacheControl
46+
47+
if (!revalidate) {
48+
return ''
49+
}
50+
51+
const readableRevalidate = humanReadableTimeRounded(revalidate)
52+
const readableExpire = expire ? humanReadableTimeRounded(expire) : '∞'
53+
54+
return `${readableRevalidate} / ${readableExpire}`
55+
}

packages/next/src/build/utils.ts

+35-23
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import { buildAppStaticPaths } from './static-paths/app'
8282
import { buildPagesStaticPaths } from './static-paths/pages'
8383
import type { PrerenderedRoute } from './static-paths/types'
8484
import type { CacheControl } from '../server/lib/cache-control'
85+
import { formatCacheControl } from './output/format'
8586

8687
export type ROUTER_TYPE = 'pages' | 'app'
8788

@@ -347,7 +348,6 @@ export interface PageInfo {
347348
*/
348349
isRoutePPREnabled: boolean
349350
ssgPageRoutes: string[] | null
350-
// TODO: initialCacheControl should be set per prerendered route.
351351
initialCacheControl: CacheControl | undefined
352352
pageDuration: number | undefined
353353
ssgPageDurations: number[] | undefined
@@ -447,7 +447,9 @@ export async function printTreeView(
447447
// Collect all the symbols we use so we can print the icons out.
448448
const usedSymbols = new Set()
449449

450-
const messages: [string, string, string][] = []
450+
const messages: [string, string, string, string][] = []
451+
452+
let showCacheLife = false
451453

452454
const stats = await computeFromManifest(
453455
{ build: buildManifest, app: appBuildManifest },
@@ -468,12 +470,17 @@ export async function printTreeView(
468470
return
469471
}
470472

473+
showCacheLife = filteredPages.some(
474+
(page) => pageInfos.get(page)?.initialCacheControl?.revalidate
475+
)
476+
471477
messages.push(
472478
[
473479
routerType === 'app' ? 'Route (app)' : 'Route (pages)',
474480
'Size',
475481
'First Load JS',
476-
].map((entry) => underline(entry)) as [string, string, string]
482+
showCacheLife ? 'Cache Life' : '',
483+
].map((entry) => underline(entry)) as [string, string, string, string]
477484
)
478485

479486
filteredPages.forEach((item, i, arr) => {
@@ -522,16 +529,8 @@ export async function printTreeView(
522529

523530
usedSymbols.add(symbol)
524531

525-
// TODO: Rework this to be usable for app router routes.
526-
// See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
527-
if (pageInfo?.initialCacheControl?.revalidate) usedSymbols.add('ISR')
528-
529532
messages.push([
530-
`${border} ${symbol} ${
531-
pageInfo?.initialCacheControl?.revalidate
532-
? `${item} (ISR: ${pageInfo?.initialCacheControl.revalidate} Seconds)`
533-
: item
534-
}${
533+
`${border} ${symbol} ${item}${
535534
totalDuration > MIN_DURATION
536535
? ` (${getPrettyDuration(totalDuration)})`
537536
: ''
@@ -550,6 +549,9 @@ export async function printTreeView(
550549
? getPrettySize(pageInfo.totalSize, { strong: true })
551550
: ''
552551
: '',
552+
showCacheLife && pageInfo?.initialCacheControl
553+
? formatCacheControl(pageInfo.initialCacheControl)
554+
: '',
553555
])
554556

555557
const uniqueCssFiles =
@@ -569,6 +571,7 @@ export async function printTreeView(
569571
`${contSymbol} ${innerSymbol} ${getCleanName(file)}`,
570572
typeof size === 'number' ? getPrettySize(size) : '',
571573
'',
574+
'',
572575
])
573576
})
574577
}
@@ -623,6 +626,10 @@ export async function printTreeView(
623626
routes.forEach(
624627
({ route, duration, avgDuration }, index, { length }) => {
625628
const innerSymbol = index === length - 1 ? '└' : '├'
629+
630+
const initialCacheControl =
631+
pageInfos.get(route)?.initialCacheControl
632+
626633
messages.push([
627634
`${contSymbol} ${innerSymbol} ${route}${
628635
duration > MIN_DURATION
@@ -635,6 +642,9 @@ export async function printTreeView(
635642
}`,
636643
'',
637644
'',
645+
showCacheLife && initialCacheControl
646+
? formatCacheControl(initialCacheControl)
647+
: '',
638648
])
639649
}
640650
)
@@ -653,6 +663,7 @@ export async function printTreeView(
653663
? getPrettySize(sharedFilesSize, { strong: true })
654664
: '',
655665
'',
666+
'',
656667
])
657668
const sharedCssFiles: string[] = []
658669
const sharedJsChunks = [
@@ -686,14 +697,20 @@ export async function printTreeView(
686697
return
687698
}
688699

689-
messages.push([` ${innerSymbol} ${cleanName}`, getPrettySize(size), ''])
700+
messages.push([
701+
` ${innerSymbol} ${cleanName}`,
702+
getPrettySize(size),
703+
'',
704+
'',
705+
])
690706
})
691707

692708
if (restChunkCount > 0) {
693709
messages.push([
694710
` └ other shared chunks (total)`,
695711
getPrettySize(restChunkSize),
696712
'',
713+
'',
697714
])
698715
}
699716
}
@@ -705,7 +722,7 @@ export async function printTreeView(
705722
list: lists.app,
706723
})
707724

708-
messages.push(['', '', ''])
725+
messages.push(['', '', '', ''])
709726
}
710727

711728
pageInfos.set('/404', {
@@ -735,17 +752,18 @@ export async function printTreeView(
735752
.map(gzipSize ? fsStatGzip : fsStat)
736753
)
737754

738-
messages.push(['', '', ''])
755+
messages.push(['', '', '', ''])
739756
messages.push([
740757
'ƒ Middleware',
741758
getPrettySize(sum(middlewareSizes), { strong: true }),
742759
'',
760+
'',
743761
])
744762
}
745763

746764
print(
747765
textTable(messages, {
748-
align: ['l', 'l', 'r'],
766+
align: ['l', 'r', 'r', 'r'],
749767
stringLength: (str) => stripAnsi(str).length,
750768
})
751769
)
@@ -766,19 +784,13 @@ export async function printTreeView(
766784
'(SSG)',
767785
`prerendered as static HTML (uses ${cyan(staticFunctionInfo)})`,
768786
],
769-
usedSymbols.has('ISR') && [
770-
'',
771-
'(ISR)',
772-
`incremental static regeneration (uses revalidate in ${cyan(
773-
staticFunctionInfo
774-
)})`,
775-
],
776787
usedSymbols.has('◐') && [
777788
'◐',
778789
'(Partial Prerender)',
779790
'prerendered as static HTML with dynamic server-streamed content',
780791
],
781792
usedSymbols.has('ƒ') && ['ƒ', '(Dynamic)', `server-rendered on demand`],
793+
showCacheLife && ['', '(Cache Life)', 'revalidate / expire'],
782794
].filter((x) => x) as [string, string, string][],
783795
{
784796
align: ['l', 'l', 'l'],

test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts

+24-27
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,36 @@ describe('build-output-tree-view', () => {
1414
beforeAll(() => next.build())
1515

1616
it('should show info about prerendered and dynamic routes in a tree view', async () => {
17-
// TODO: Show cache info (revalidate/expire) for app router, and use the
18-
// same for pages router instead of the ISR addendum.
19-
2017
// TODO: Fix double-listing of the /ppr/[slug] fallback.
2118

2219
expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
23-
"Route (app) Size First Load JS
24-
┌ ○ /_not-found N/A kB N/A kB
25-
├ ƒ /api N/A kB N/A kB
26-
├ ○ /api/force-static N/A kB N/A kB
27-
├ ○ /app-static N/A kB N/A kB
28-
├ ○ /cache-life N/A kB N/A kB
29-
├ ƒ /dynamic N/A kB N/A kB
30-
├ ◐ /ppr/[slug] N/A kB N/A kB
31-
├ ├ /ppr/[slug]
32-
├ ├ /ppr/[slug]
33-
├ ├ /ppr/days
34-
├ └ /ppr/weeks
35-
└ ○ /revalidate N/A kB N/A kB
36-
+ First Load JS shared by all N/A kB
20+
"Route (app) Size First Load JS Cache Life
21+
┌ ○ /_not-found N/A kB N/A kB
22+
├ ƒ /api N/A kB N/A kB
23+
├ ○ /api/force-static N/A kB N/A kB
24+
├ ○ /app-static N/A kB N/A kB
25+
├ ○ /cache-life N/A kB N/A kB 1 h / 1 d
26+
├ ƒ /dynamic N/A kB N/A kB
27+
├ ◐ /ppr/[slug] N/A kB N/A kB 1 w / 30 d
28+
├ ├ /ppr/[slug] 1 w / 30 d
29+
├ ├ /ppr/[slug] 1 w / 30 d
30+
├ ├ /ppr/days 1 d / 1 w
31+
├ └ /ppr/weeks 1 w / 30 d
32+
└ ○ /revalidate N/A kB N/A kB 15 min / 1 y
33+
+ First Load JS shared by all N/A kB
3734
38-
Route (pages) Size First Load JS
39-
┌ ƒ /api/hello N/A kB N/A kB
40-
├ ● /gsp-revalidate (ISR: 300 Seconds) N/A kB N/A kB
41-
├ ƒ /gssp N/A kB N/A kB
42-
└ ○ /static N/A kB N/A kB
43-
+ First Load JS shared by all N/A kB
35+
Route (pages) Size First Load JS Cache Life
36+
┌ ƒ /api/hello N/A kB N/A kB
37+
├ ● /gsp-revalidate N/A kB N/A kB 5 min / 1 y
38+
├ ƒ /gssp N/A kB N/A kB
39+
└ ○ /static N/A kB N/A kB
40+
+ First Load JS shared by all N/A kB
4441
4542
○ (Static) prerendered as static content
4643
● (SSG) prerendered as static HTML (uses generateStaticParams)
47-
(ISR) incremental static regeneration (uses revalidate in generateStaticParams)
4844
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
49-
ƒ (Dynamic) server-rendered on demand"
45+
ƒ (Dynamic) server-rendered on demand
46+
(Cache Life) revalidate / expire"
5047
`)
5148
})
5249
})
@@ -64,12 +61,12 @@ describe('build-output-tree-view', () => {
6461

6562
it('should show info about prerendered routes in a compact tree view', async () => {
6663
expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
67-
"Route (app) Size First Load JS
64+
"Route (app) Size First Load JS
6865
┌ ○ / N/A kB N/A kB
6966
└ ○ /_not-found N/A kB N/A kB
7067
+ First Load JS shared by all N/A kB
7168
72-
Route (pages) Size First Load JS
69+
Route (pages) Size First Load JS
7370
─ ○ /static N/A kB N/A kB
7471
+ First Load JS shared by all N/A kB
7572

test/production/app-dir/build-output-tree-view/fixtures/mixed/app/cache-life/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { unstable_cacheLife } from 'next/cache'
44

55
export default async function Page() {
6-
unstable_cacheLife('weeks')
6+
unstable_cacheLife('hours')
77

88
return <p>hello world</p>
99
}

0 commit comments

Comments
 (0)