Skip to content

Commit

Permalink
Show cache life column in build output
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable committed Feb 22, 2025
1 parent 488cbf5 commit 481e399
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 62 deletions.
12 changes: 2 additions & 10 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2905,23 +2905,15 @@ export default async function build(
...(pageInfos.get(route.pathname) as PageInfo),
hasPostponed,
hasEmptyPrelude,
// TODO: Enable the following line to show "ISR" status in build
// output. Requires different presentation to also work for app
// router routes.
// See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
// initialCacheControl: cacheControl,
initialCacheControl: cacheControl,
})

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

if (cacheControl.revalidate !== 0) {
Expand Down
55 changes: 55 additions & 0 deletions packages/next/src/build/output/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { CacheControl } from '../../server/lib/cache-control'

const timeUnits = [
{ label: 'y', seconds: 31536000 },
{ label: 'w', seconds: 604800 },
{ label: 'd', seconds: 86400 },
{ label: 'h', seconds: 3600 },
{ label: 'min', seconds: 60 },
{ label: 's', seconds: 1 },
]

function humanReadableTimeRounded(seconds: number): string {
// Find the largest fitting unit.
let candidateIndex = timeUnits.length - 1
for (let i = 0; i < timeUnits.length; i++) {
if (seconds >= timeUnits[i].seconds) {
candidateIndex = i
break
}
}

const candidate = timeUnits[candidateIndex]
const value = seconds / candidate.seconds
const isExact = Number.isInteger(value)

// For days and weeks only, check if using the next smaller unit yields an
// exact result.
if (!isExact && (candidate.label === 'd' || candidate.label === 'w')) {
const nextUnit = timeUnits[candidateIndex + 1]
const nextValue = seconds / nextUnit.seconds

if (Number.isInteger(nextValue)) {
return `${nextValue} ${nextUnit.label}`
}
}

if (isExact) {
return `${value} ${candidate.label}`
}

return `~${Math.round(value)} ${candidate.label}`
}

export function formatCacheControl(cacheControl: CacheControl): string {
const { revalidate, expire } = cacheControl

if (!revalidate) {
return ''
}

const readableRevalidate = humanReadableTimeRounded(revalidate)
const readableExpire = expire ? humanReadableTimeRounded(expire) : '∞'

return `${readableRevalidate} / ${readableExpire}`
}
58 changes: 35 additions & 23 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import { buildAppStaticPaths } from './static-paths/app'
import { buildPagesStaticPaths } from './static-paths/pages'
import type { PrerenderedRoute } from './static-paths/types'
import type { CacheControl } from '../server/lib/cache-control'
import { formatCacheControl } from './output/format'

export type ROUTER_TYPE = 'pages' | 'app'

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

const messages: [string, string, string][] = []
const messages: [string, string, string, string][] = []

let showCacheLife = false

const stats = await computeFromManifest(
{ build: buildManifest, app: appBuildManifest },
Expand All @@ -468,12 +470,17 @@ export async function printTreeView(
return
}

showCacheLife = filteredPages.some(
(page) => pageInfos.get(page)?.initialCacheControl?.revalidate
)

messages.push(
[
routerType === 'app' ? 'Route (app)' : 'Route (pages)',
'Size',
'First Load JS',
].map((entry) => underline(entry)) as [string, string, string]
showCacheLife ? 'Cache Life' : '',
].map((entry) => underline(entry)) as [string, string, string, string]
)

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

usedSymbols.add(symbol)

// TODO: Rework this to be usable for app router routes.
// See https://vercel.slack.com/archives/C02CDC2ALJH/p1739552318644119?thread_ts=1739550179.439319&cid=C02CDC2ALJH
if (pageInfo?.initialCacheControl?.revalidate) usedSymbols.add('ISR')

messages.push([
`${border} ${symbol} ${
pageInfo?.initialCacheControl?.revalidate
? `${item} (ISR: ${pageInfo?.initialCacheControl.revalidate} Seconds)`
: item
}${
`${border} ${symbol} ${item}${
totalDuration > MIN_DURATION
? ` (${getPrettyDuration(totalDuration)})`
: ''
Expand All @@ -550,6 +549,9 @@ export async function printTreeView(
? getPrettySize(pageInfo.totalSize, { strong: true })
: ''
: '',
showCacheLife && pageInfo?.initialCacheControl
? formatCacheControl(pageInfo.initialCacheControl)
: '',
])

const uniqueCssFiles =
Expand All @@ -569,6 +571,7 @@ export async function printTreeView(
`${contSymbol} ${innerSymbol} ${getCleanName(file)}`,
typeof size === 'number' ? getPrettySize(size) : '',
'',
'',
])
})
}
Expand Down Expand Up @@ -623,6 +626,10 @@ export async function printTreeView(
routes.forEach(
({ route, duration, avgDuration }, index, { length }) => {
const innerSymbol = index === length - 1 ? '└' : '├'

const initialCacheControl =
pageInfos.get(route)?.initialCacheControl

messages.push([
`${contSymbol} ${innerSymbol} ${route}${
duration > MIN_DURATION
Expand All @@ -635,6 +642,9 @@ export async function printTreeView(
}`,
'',
'',
showCacheLife && initialCacheControl
? formatCacheControl(initialCacheControl)
: '',
])
}
)
Expand All @@ -653,6 +663,7 @@ export async function printTreeView(
? getPrettySize(sharedFilesSize, { strong: true })
: '',
'',
'',
])
const sharedCssFiles: string[] = []
const sharedJsChunks = [
Expand Down Expand Up @@ -686,14 +697,20 @@ export async function printTreeView(
return
}

messages.push([` ${innerSymbol} ${cleanName}`, getPrettySize(size), ''])
messages.push([
` ${innerSymbol} ${cleanName}`,
getPrettySize(size),
'',
'',
])
})

if (restChunkCount > 0) {
messages.push([
` └ other shared chunks (total)`,
getPrettySize(restChunkSize),
'',
'',
])
}
}
Expand All @@ -705,7 +722,7 @@ export async function printTreeView(
list: lists.app,
})

messages.push(['', '', ''])
messages.push(['', '', '', ''])
}

pageInfos.set('/404', {
Expand Down Expand Up @@ -735,17 +752,18 @@ export async function printTreeView(
.map(gzipSize ? fsStatGzip : fsStat)
)

messages.push(['', '', ''])
messages.push(['', '', '', ''])
messages.push([
'ƒ Middleware',
getPrettySize(sum(middlewareSizes), { strong: true }),
'',
'',
])
}

print(
textTable(messages, {
align: ['l', 'l', 'r'],
align: ['l', 'r', 'r', 'r'],
stringLength: (str) => stripAnsi(str).length,
})
)
Expand All @@ -766,19 +784,13 @@ export async function printTreeView(
'(SSG)',
`prerendered as static HTML (uses ${cyan(staticFunctionInfo)})`,
],
usedSymbols.has('ISR') && [
'',
'(ISR)',
`incremental static regeneration (uses revalidate in ${cyan(
staticFunctionInfo
)})`,
],
usedSymbols.has('◐') && [
'◐',
'(Partial Prerender)',
'prerendered as static HTML with dynamic server-streamed content',
],
usedSymbols.has('ƒ') && ['ƒ', '(Dynamic)', `server-rendered on demand`],
showCacheLife && ['', '(Cache Life)', 'revalidate / expire'],
].filter((x) => x) as [string, string, string][],
{
align: ['l', 'l', 'l'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,36 @@ describe('build-output-tree-view', () => {
beforeAll(() => next.build())

it('should show info about prerendered and dynamic routes in a tree view', async () => {
// TODO: Show cache info (revalidate/expire) for app router, and use the
// same for pages router instead of the ISR addendum.

// TODO: Fix double-listing of the /ppr/[slug] fallback.

expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
"Route (app) Size First Load JS
┌ ○ /_not-found 42 kB 42 kB
├ ƒ /api 42 kB 42 kB
├ ○ /api/force-static 42 kB 42 kB
├ ○ /app-static 42 kB 42 kB
├ ○ /cache-life 42 kB 42 kB
├ ƒ /dynamic 42 kB 42 kB
├ ◐ /ppr/[slug] 42 kB 42 kB
├ ├ /ppr/[slug]
├ ├ /ppr/[slug]
├ ├ /ppr/days
├ └ /ppr/weeks
└ ○ /revalidate 42 kB 42 kB
+ First Load JS shared by all 42 kB
Route (pages) Size First Load JS
┌ ƒ /api/hello 42 kB 42 kB
├ ● /gsp-revalidate (ISR: 300 Seconds) 42 kB 42 kB
├ ƒ /gssp 42 kB 42 kB
└ ○ /static 42 kB 42 kB
+ First Load JS shared by all 42 kB
"Route (app) Size First Load JS Cache Life
┌ ○ /_not-found 42 kB 42 kB
├ ƒ /api 42 kB 42 kB
├ ○ /api/force-static 42 kB 42 kB
├ ○ /app-static 42 kB 42 kB
├ ○ /cache-life 42 kB 42 kB 1 h / 1 d
├ ƒ /dynamic 42 kB 42 kB
├ ◐ /ppr/[slug] 42 kB 42 kB 1 w / 30 d
├ ├ /ppr/[slug] 1 w / 30 d
├ ├ /ppr/[slug] 1 w / 30 d
├ ├ /ppr/days 1 d / 1 w
├ └ /ppr/weeks 1 w / 30 d
└ ○ /revalidate 42 kB 42 kB 15 min / 1 y
+ First Load JS shared by all 42 kB
Route (pages) Size First Load JS Cache Life
┌ ƒ /api/hello 42 kB 42 kB
├ ● /gsp-revalidate 42 kB 42 kB 5 min / 1 y
├ ƒ /gssp 42 kB 42 kB
└ ○ /static 42 kB 42 kB
+ First Load JS shared by all 42 kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
(ISR) incremental static regeneration (uses revalidate in generateStaticParams)
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
ƒ (Dynamic) server-rendered on demand"
ƒ (Dynamic) server-rendered on demand
(Cache Life) revalidate / expire"
`)
})
})
Expand All @@ -64,12 +61,12 @@ describe('build-output-tree-view', () => {

it('should show info about prerendered routes in a compact tree view', async () => {
expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(`
"Route (app) Size First Load JS
"Route (app) Size First Load JS
┌ ○ / 42 kB 42 kB
└ ○ /_not-found 42 kB 42 kB
+ First Load JS shared by all 42 kB
Route (pages) Size First Load JS
Route (pages) Size First Load JS
─ ○ /static 42 kB 42 kB
+ First Load JS shared by all 42 kB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { unstable_cacheLife } from 'next/cache'

export default async function Page() {
unstable_cacheLife('weeks')
unstable_cacheLife('hours')

return <p>hello world</p>
}

0 comments on commit 481e399

Please sign in to comment.