next-browser by vercel-labs/next-browser
npx skills add https://github.com/vercel-labs/next-browser --skill next-browser如果 next-browser 尚未在 PATH 中,请使用用户的包管理器全局安装 @vercel/next-browser,然后运行 playwright install chromium。
如果 next-browser 已安装,可能已过时。运行 next-browser --version 并与 npm 上的最新版本(npm view @vercel/next-browser version)进行比较。如果已安装版本落后,请先升级(npm install -g @vercel/next-browser@latest 或用户包管理器的等效命令),然后再继续。
如果项目的 Next.js 版本是 v16.2.0-canary.37 或更高版本,捆绑的文档位于 node_modules/next/dist/docs/。在进行 PPR 工作、缓存组件工作或任何重要的 Next.js 任务之前,请先阅读相关文档——你的训练数据可能已过时。捆绑的文档是唯一可信的来源。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
open 并开始。[{"name":"session","value":"..."}],或者 (2) 直接从 DevTools → Network 中任意经过身份验证的请求上 "Copy as cURL"——你可以自己从请求头中提取 cookies。project、读取配置)。screenshot。始终为其添加说明(screenshot "修复前",screenshot "PPR 外壳 — 已锁定")。在 headed 模式下,截图日志窗口会自动打开,因此用户可以实时看到每个截图。默认情况下,浏览器以 headed 模式(可见窗口)打开。对于没有显示器的 CI 或云环境,设置 NEXT_BROWSER_HEADLESS=1 以无头模式运行。
open <url> [--cookies-json <file>]启动浏览器,导航到 URL。使用 --cookies-json 时,会在导航前设置身份验证 cookies(域名从 URL 主机名派生)。
$ next-browser open http://localhost:3024/vercel --cookies-json cookies.json
opened → http://localhost:3024/vercel (11 cookies for localhost)
Cookie 文件格式:[{"name":"authorization","value":"Bearer ..."}, ...]
每个 cookie 只需要 name 和 value——省略 domain、path、expires 等。要创建该文件,请使用 Bash(echo '[...]' > /tmp/cookies.json),因为 Write 工具需要先进行 Read 操作。
close关闭浏览器并终止守护进程。
goto <url>导航到 URL 并进行全新的服务器渲染。浏览器加载一个新文档——相当于在地址栏中输入 URL。
$ next-browser goto http://localhost:3024/vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments
push [path]客户端导航——页面转换无需完全重新加载,就像用户在应用中点击链接一样。如果没有指定路径,则显示当前页面上所有链接的交互式选择器。
$ next-browser push /vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments
如果 push 静默失败(URL 未改变),则说明该路由未被预取。
back在浏览器历史记录中后退一页。
reload从服务器重新加载当前页面。
ssr lock阻止所有后续导航中的外部脚本。锁定后,每次 goto、push、back 和 reload 都会显示原始的服务器渲染 HTML,没有 React 水合或客户端 JavaScript——这是搜索引擎和社交爬虫看到的内容。
$ next-browser ssr lock
ssr locked — external scripts blocked on all navigations
ssr unlock重新启用外部脚本。下一次导航将正常加载并完全水合。
$ next-browser ssr unlock
ssr unlocked — external scripts re-enabled
perf [url]分析完整页面加载——重新加载当前页面(或导航到 URL)并一次性收集核心 Web 指标和 React 水合计时。
$ next-browser perf http://localhost:3000/dashboard
# Page Load Profile — http://localhost:3000/dashboard
## Core Web Vitals
TTFB 42ms
LCP 1205.3ms (img: /_next/image?url=...)
CLS 0.03
## React Hydration — 65.5ms (466.2ms → 531.7ms)
Hydrated 65.5ms (466.2 → 531.7)
Commit 2.0ms (531.7 → 533.7)
Waiting for Paint 3.0ms (533.7 → 536.7)
Remaining Effects 4.1ms (536.7 → 540.8)
## Hydrated components (42 total, sorted by duration)
DeploymentsProvider 8.3ms
NavigationProvider 5.1ms
...
TTFB——服务器响应时间(Navigation Timing API)。LCP——最大可见元素绘制的时间,以及它是什么。CLS——累积布局偏移分数(越低越好)。Hydration——React 协调器阶段和每个组件的成本(需要 React 性能分析构建 / next dev;生产构建会剥离 console.timeStamp)。
如果没有 URL,则重新加载当前页面。如果有 URL,则先导航到该页面。
renders start开始记录 React 重新渲染。钩入 onCommitFiberRoot 以收集原始的每个组件数据:渲染次数、totalTime、selfTime、DOM 变更、变更原因和 FPS。
在整页导航(goto/reload)后仍然有效,并捕获挂载和水合渲染——无需在导航前后启动。
$ next-browser renders start
recording renders — interact with the page, then run `renders stop`
renders stop [--json]停止记录并打印每个组件的渲染性能分析。原始数据——由代理决定哪些是可操作的。
$ next-browser renders stop
# Render Profile — 3.05s recording
# 426 renders (38 mounts + 388 re-renders) across 38 components
# FPS: avg 120, min 106, max 137, drops (<30fps): 0
## Components by total render time
| Component | Insts | Mounts | Re-renders | Total | Self | DOM | Top change reason |
| ---------------------- | ----- | ------ | ---------- | -------- | -------- | ----- | -------------------------- |
| Parent | 1 | 1 | 9 | 5.8ms | 3.4ms | 10/10 | state (hook #0) |
| MemoChild | 3 | 3 | 27 | 2ms | 1.9ms | 30/30 | props.data |
| Router | 1 | 1 | 9 | 6.3ms | — | 0/10 | parent (ErrorBoundaryHandler) |
## Change details (prev → next)
Parent
state (hook #0): 0 → 1
MemoChild
props.data: {value} → {value}
变更详情 部分显示了每次变更的实际前后值。这使得数据自包含——你可以看到 MemoChild 获取了 props.data: {value} → {value}(相同结构,新引用——memo 失效),而无需检查组件。
使用 --json,输出原始结构化 JSON,包含每个组件的完整变更数组(每个渲染事件的类型、名称、前值、后值)。
列说明:
Insts——记录期间观察到的唯一组件实例数Mounts——实例挂载的次数(首次渲染,无备用 fiber)Re-renders——更新阶段的渲染次数(总渲染次数减去挂载次数)Total——包含子组件的渲染时间(组件 + 子组件)Self——不包含子组件的渲染时间(仅组件本身)DOM——实际变更了 DOM 的渲染次数与总渲染次数的对比Top change reason——此组件最频繁的触发原因计时数据(Total、Self)需要 React 性能分析构建(next dev)。在生产构建中,这些列显示 —,但渲染次数、DOM 变更和变更原因仍会报告。
变更原因——触发每次重新渲染的原因:
props.<name>——某个属性通过引用发生了更改,包含前后值state (hook #N)——某个 useState/useReducer 钩子发生了更改,包含前后值context (<name>)——某个特定上下文发生了更改,包含前后值parent (<name>)——父组件重新渲染,命名父组件parent (<name> (mount))——父组件也在挂载(页面加载期间常见,不是泄漏)mount——首次渲染FPS——记录期间的每秒帧数。drops 统计低于 30fps 的帧数。
最多跟踪 200 个组件。如果输出超过 4000 个字符,则写入临时文件。
restart-server重启 Next.js 开发服务器并清除其缓存。强制从头开始重新编译。
最后的手段。HMR 会自动获取代码更改——仅当有证据表明开发服务器卡住时(编辑后输出陈旧、构建永不完成、错误未清除)才使用此命令。
通常会以 net::ERR_ABORTED 退出——这是预期的(页面在重启期间分离)。服务器恢复后,使用 goto <url> 重新导航。不要将此错误视为失败。
ppr lock前提条件: PPR 需要在 next.config 中启用 cacheComponents。否则,外壳将没有预渲染内容可显示。
冻结动态内容,以便检查静态外壳——在加载任何数据之前立即可用的页面部分。锁定后:
goto——显示服务器渲染的外壳,动态内容应出现的位置显示为空洞。push——显示客户端已通过预取获取的内容。要求当前页面已经水合(预取是客户端行为),因此请在到达源页面 之后 锁定,而不是之前。$ next-browser ppr lock
locked
ppr unlock恢复动态内容并打印外壳分析——哪些 Suspense 边界是外壳中的空洞、是什么阻止了它们、哪些是静态的。输出可能非常大(数百个边界)。如果只需要摘要和动态空洞,可以通过管道传递 | head -20。
$ next-browser ppr unlock
unlocked
# PPR Shell Analysis
# 131 boundaries: 3 dynamic holes, 128 static
## Summary
- Top actionable hole: TrackedSuspense — usePathname (client-hook)
- Suggested next step: This route segment is suspending on client hooks. Check loading.tsx first...
- Most common root cause: usePathname (client-hook) affecting 1 boundary
## Quick Reference
| Boundary | Type | Fallback source | Primary blocker | Source | Suggested next step |
| --- | --- | --- | --- | --- | --- |
| TrackedSuspense | component | unknown | usePathname (client-hook) | tracked-suspense.js:6 | Push the hook-using cl... |
| TeamDeploymentsLayout | route-segment | unknown | unknown | layout.tsx:37 | Inspect the nearest us... |
| Next.Metadata | component | unknown | unknown | unknown | No primary blocker was... |
## Detail
TrackedSuspense
rendered by: TrackedSuspense > RootLayout > AppLayout
environments: SSR
TeamDeploymentsLayout
suspenders unknown: thrown Promise (library using throw instead of use())
## Static (pre-rendered in shell)
GeistProvider at .../geist-provider.tsx:80:9
TrackedSuspense at ...
...
快速参考 表是主要概览——边界、阻塞器、来源和建议的修复方法一目了然。详情 部分仅出现在具有额外信息(所有者链、环境、次要阻塞器)且未在表格中显示的孔洞中。
锁定期间 errors 不报告。 如果外壳看起来不对(为空,回退到 CSR),请解锁并正常 goto 页面,然后运行 errors。不要在锁定状态下盲目调试。
完全回退(scrollHeight = 0)。 当 PPR 完全回退时,unlock 仅返回 "unlocked",没有外壳分析——没有边界可报告。在这种情况下,解锁,正常 goto 页面,然后使用 errors 和 logs 查找根本原因。
tree完整的 React 组件树——页面上的每个组件及其层次结构,类似于 React DevTools 中的 Components 面板。
$ next-browser tree
# React component tree
# Columns: depth id parent name [key=...]
# Use `tree <id>` for props/hooks/state. IDs valid until next navigation.
0 38167 - Root
1 38168 38167 HeadManagerContext.Provider
2 38169 38168 Root
...
224 46375 46374 DeploymentsProvider
226 46506 46376 DeploymentsTable
tree <id>检查一个组件:祖先路径、属性、钩子、状态、源代码位置(已源映射到原始文件)。
$ next-browser tree 46375
path: Root > ... > Prerender(TeamDeploymentsPage) > Prerender(FullHeading) > Prerender(TrackedSuspense) > Suspense > DeploymentsProvider
DeploymentsProvider #46375
props:
children: [<Lazy />, <Lazy />, <span />, <Lazy />, <Lazy />]
hooks:
IsMobile: undefined (1 sub)
Router: undefined (2 sub)
DeploymentListScope: undefined (1 sub)
User: undefined (4 sub)
Team: undefined (4 sub)
...
DeploymentsInfinite: undefined (12 sub)
source: app/(dashboard)/[teamSlug]/(team)/~/deployments/_parts/context.tsx:180:10
ID 在导航前有效。在 goto/push 后重新运行 tree。
viewport [WxH]显示或设置浏览器视口大小。用于测试响应式布局。
$ next-browser viewport
1440x900
$ next-browser viewport 375x812
viewport set to 375x812
设置后,视口大小在导航过程中保持固定。通过 eval 使用 window.resizeTo() 在 Playwright 中无效——始终使用此命令更改尺寸。
screenshot [caption] [--full-page]行为规则见 与用户协作 → 展示,而非讲述。
仅当视觉布局很重要(CSS、外观、PPR 外壳)时才使用 screenshot。对于页面内容或决定点击什么,请使用 snapshot。
捕获视口(或使用 --full-page 捕获完整可滚动页面)到临时 PNG 文件并返回路径。在 headed 模式下,每个截图都会添加到 截图日志——一个实时浏览器窗口,累积会话期间拍摄的所有截图。在 headless 模式下,跳过日志窗口。
可选的说明描述了截图或拍摄截图的原因。说明显示在截图日志中每个图像的上方。
$ next-browser screenshot "Homepage after login"
/tmp/next-browser-1711234567890.png
$ next-browser screenshot "Full page layout" --full-page
/tmp/next-browser-1711234567891.png
snapshot快照页面的无障碍树——屏幕阅读器看到的语义结构——每个交互元素上都有 [ref=eN] 标记。在点击之前使用此功能来发现页面上的内容。
$ next-browser snapshot
- navigation "Main"
- link "Home" [ref=e0]
- link "Dashboard" [ref=e1]
- main
- heading "Settings"
- tablist
- tab "General" [ref=e2] (selected)
- tab "Security" [ref=e3]
- region "Profile"
- textbox "Username" [ref=e4]
- button "Save" [ref=e5]
该树显示标题、地标(navigation、main、region)和状态(selected、checked、expanded、disabled),以便你理解页面布局,而不仅仅是扁平的元素列表。
引用是临时的——每次调用 snapshot 时都会重置,并且在导航后无效。在 goto/push 后重新运行 snapshot。
click <ref|text|selector>使用真实的指针事件(pointerdown → mousedown → pointerup → mouseup → click)点击元素。这适用于忽略合成 .click() 的库(Radix UI、Headless UI 等)。
三种定位方式:
| 输入 | 示例 | 解析方式 |
|---|---|---|
| 来自树的引用 | click e3 | 从上次快照中查找角色+名称 |
| 纯文本 | click "Security" | Playwright text=Security 选择器 |
| Playwright 选择器 | click "#submit-btn" | 按原样使用(CSS、role= 等) |
推荐工作流程: 先运行 snapshot,然后 click eN。引用是最可靠的——它们通过 ARIA 角色+名称解析,因此即使元素没有稳定的 CSS 选择器也能工作。
点击导航链接可能超时。 在 Next.js <Link> 上 click 会等待导航完成,这可能超过命令超时时间。如果 click 在导航链接上挂起,请取消它并使用 goto <url> 代替。
$ next-browser snapshot
- tablist
- tab "General" [ref=e0] (selected)
- tab "Security" [ref=e1]
$ next-browser click e1
clicked
$ next-browser snapshot
- tablist
- tab "General" [ref=e0]
- tab "Security" [ref=e1] (selected)
fill <ref|selector> <value>填充文本输入框或文本区域。清除现有内容,然后键入新值——分发 React 和其他框架期望的所有事件。
$ next-browser snapshot
- textbox "Username" [ref=e4]
$ next-browser fill e4 "judegao"
filled
eval [ref] <script> · eval [ref] --file <path> · eval -在页面上下文中运行 JS。以 JSON 格式返回结果。
使用引用 时,脚本将 DOM 元素作为其参数接收——对于检查快照节点或桥接到 React 内部很有用:
$ next-browser eval e0 'el => el.tagName'
"BUTTON"
$ next-browser eval e0 'el => {
const key = Object.keys(el).find(k => k.startsWith("__reactFiber$"));
if (!key) return null;
let fiber = el[key];
while (fiber && typeof fiber.type !== "function") fiber = fiber.return;
return fiber?.type?.displayName || fiber?.type?.name || null;
}'
"LoginButton"
对于简单的单行脚本(无引用),内联传递脚本:
$ next-browser eval 'document.title'
"Deployments – Vercel"
$ next-browser eval 'document.querySelectorAll("a[href]").length'
47
对于多行或引号较多的脚本,使用 --file(或 -f)完全避免 shell 引用问题:
cat > /tmp/nb-eval.js << 'SCRIPT'
(() => {
// your JS here — no shell escaping needed
return someResult;
})()
SCRIPT
next-browser eval --file /tmp/nb-eval.js
你也可以通过 stdin 管道传递:echo 'document.title' | next-browser eval -
使用此命令读取 Next.js 错误覆盖层(它在 shadow DOM 中):next-browser eval 'document.querySelector("nextjs-portal")?.shadowRoot?.querySelector("[data-nextjs-dialog]")?.textContent'
eval 在页面上下文中同步运行——不支持顶级 await。如果需要等待,请包装在异步 IIFE 中:next-browser eval '(async () => { ... })()'。
errors当前页面的构建时和运行时错误。
$ next-browser errors
{
"configErrors": [],
"sessionErrors": [
{
"url": "/vercel/~/deployments",
"buildError": null,
"runtimeErrors": [
{
"type": "console",
"errorName": "Error",
"message": "Route \"/[teamSlug]/~/deployments\": Uncached data or `connection()` was accessed outside of `<Suspense>`...",
"stack": [
{"file": "app/(dashboard)/.../deployments.tsx", "methodName": "Deployments", "line": 105, "column": 27}
]
}
]
}
]
}
buildError 是编译失败。runtimeErrors 包含 type: "runtime"(React 错误)和 type: "console"(console.error 调用)。
logs最近的开发服务器日志输出。
$ next-browser logs
{"timestamp":"00:01:55.381","source":"Server","level":"WARN","message":"[browser] navigation-metrics: skeleton visible was already recorded..."}
{"timestamp":"00:01:55.382","source":"Browser","level":"WARN","message":"navigation-metrics: content visible was already recorded..."}
browser-logs浏览器端控制台输出(console.log、console.warn、console.error、console.info)。直接从页面捕获——适用于开发和生产构建。
$ next-browser browser-logs
[LOG ] Initializing app
[WARN ] Deprecation: use fetchV2 instead
[ERROR] Failed to load resource: 404
[INFO ] render complete in 42ms
最多保留 500 条条目;缓冲区满时丢弃最旧的条目。在同一浏览器会话中,条目在导航过程中累积。如果输出超过 4000 个字符,则写入临时文件并打印路径。
何时使用哪个:
| 命令 | 来源 | 需要开发服务器 |
|---|---|---|
logs | Next.js 开发服务器 stdout | 是 |
errors | 构建错误 + console.error | 是 |
browser-logs | 所有浏览器控制台输出 | 否 |
对于开发服务器诊断,首选 logs 和 errors。当需要一般控制台输出或正在运行生产构建时,使用 browser-logs。
network列出上次导航以来的所有网络请求。
$ next-browser network
# Network requests since last navigation
# Columns: idx status method type ms url [next-action=...]
# Use `network <idx>` for headers and body.
0 200 GET document 508ms http://localhost:3024/vercel
1 200 GET font 0ms http://localhost:3024/_next/static/media/797e433ab948586e.p.d2077940.woff2
2 200 GET stylesheet 6ms http://localhost:3024/_next/static/chunks/_a17e2099._.css
3 200 GET fetch 102ms http://localhost:3024/api/v9/projects next-action=abc123def
服务器操作显示 next-action=<id> 后缀。
network <idx>一个条目的完整请求/响应。长正文会溢出到临时文件。
$ next-browser network 0
GET http://localhost:3024/vercel
type: document 508ms
request headers:
accept: text/html,...
cookie: authorization=Bearer...; isLoggedIn=1; ...
user-agent: Mozilla/5.0 ...
response: 200 OK
response headers:
cache-control: no-cache, must-revalidate
content-encoding: gzip
...
response body:
(8234 bytes written to /tmp/next-browser-12345-0.html)
page当前 URL 的路由段——哪些布局、页面和边界处于活动状态。
$ next-browser page
{
"sessions": [
{
"url": "/vercel/~/deployments",
"routerType": "app",
"segments": [
{"path": "app/(dashboard)/[teamSlug]/(team)/~/deployments/layout.tsx", "type": "layout", ...},
{"path": "app/(dashboard)/[teamSlug]/(team)/~/deployments/page.tsx", "type": "page", ...},
{"path": "app/(dashboard)/[teamSlug]/layout.tsx", "type": "layout", ...},
{"path": "app/(dashboard)/layout.tsx", "type": "layout", ...},
{"path": "app/layout.tsx", "type": "layout", ...}
]
}
]
}
project项目根目录和开发服务器 URL。
$ next-browser project
{
"projectPath": "/Users/judegao/workspace/repo/front/apps/vercel-site",
"devServerUrl": "http://localhost:3331"
}
routes所有应用路由器路由。
$ next-browser routes
{
"appRouter": [
"/[teamSlug]",
"/[teamSlug]/~/deployments",
"/[teamSlug]/[project]",
"/[teamSlug]/[project]/[id]/logs",
...
]
}
action <id>通过其 ID(来自网络列表中的 next-action 标头)检查服务器操作。
当用户说 "加载后页面很慢"、"重新渲染太多"、"交互卡顿" 或 "卡顿" 时——这是更新阶段的渲染,不是初始加载。使用 renders 进行分析。(对于初始加载,使用 perf。)
工作流程:
renders start——开始记录。goto 页面(钩子在导航后仍然有效并捕获挂载)。push 导航,或者如果问题是轮询/计时器,则只需等待。renders stop——读取原始数据。Mounts 与 Re-renders——此组件是在加载后重新渲染,还是计数仅来自挂载时的级联?Insts——高渲染计数是来自许多实例,还是一个实例渲染过多?Self——此组件每次渲染都很昂贵,还是只是调用太频繁?DOM——渲染是否实际产生了可见的变更?一个渲染 100 次但 DOM 变更 0 次的组件正在做纯粹浪费的工作。Total 与 Self——成本是在此组件中还是在其子组件中?parent (X (mount)) 是加载时的级联,不是泄漏。tree 查找组件的 ID,然后 tree <id> 获取其源文件、属性和钩子。验证修复。 编辑代码后,HMR 会获取更改。重新运行 renders start / renders stop 并将原始数字与之前的性能分析进行比较。
在提出修复建议前测试你的假设。 如果你怀疑某个组件是根本原因,请找到证据——使用 tree 检查它,阅读其源代码,通过变更原因列检查发生了什么变化。不要根据单一观察提出更改建议。
外壳是用户登陆瞬间看到的内容——在任何动态数据到达之前。衡量标准是锁定时的截图:它是否看起来像页面本身?一个外壳可以非空但仍然很差——一个包裹整个内容区域的 Suspense fallback 会渲染 一些东西,但它是一个整体的加载状态,而不是页面。
一个有意义的真正的组件树,在数据真正挂起的地方有小的、局部的 fallback。实现这一点意味着组合层——这些叶边界之间的布局和包装器——本身不能挂起。ppr unlock 的快速参考表为每个空洞命名了主要阻塞器和来源;详情部分添加了所有者链和次要阻塞器。树中高层的挂起会导致其下的所有内容折叠成一个 fallback。
自上而下地处理。对于挂起的组件:动态访问是否可以移动到子组件中?如果可以,移动它——此组件变为同步并重新加入外壳。沿着访问向下移动并再次询问。
当你到达一个无法再向下移动的组件时,有两个出口——用 Suspense 边界包装,或缓存它以进行预渲染。两者都需要人工决定(见 与用户协作 → 上报,而非决定)。
在提出修复建议前测试你的假设。 如果你怀疑某个组件是原因,请找到证据——检查 errors,使用 tree 检查组件,或者比较外壳正常工作的路由与外壳不正常的路由。不要根据单一观察就确定根本原因或提出更改建议。
根据用户到达方式的不同,有两种外壳——首先确定你正在优化哪一种(见 与用户协作 → 上报,而非决定)。
直接加载——PPR 外壳。 冷访问 URL 的服务器 HTML。先锁定,然后 goto 目标——锁定会抑制水合,因此你看到的是服务器发送的确切内容。加载完成后截图,然后解锁。
客户端导航——预取的外壳。 点击链接时路由器已经持有的内容。源页面决定这一点——它是进行预取的页面——因此 goto 源页面 未锁定 并让其完全水合。然后锁定,push 到目标,让导航完成,截图,解锁。在源页面水合之前锁定意味着没有预取任何内容,push 没有内容可显示。
迭代之间:解锁时检查 errors。
进行代码更改后: HMR 会获取它——只需重新锁定,goto 页面,并重新测试。无需 restart-server。
每周安装量
1.1K
仓库
GitHub 星标数
83
首次出现
2026年3月6日
安全审计
安装于
opencode1.0K
codex1.0K
github-copilot1.0K
gemini-cli1.0K
cursor1.0K
cline1.0K
If next-browser is not already on PATH, install @vercel/next-browser globally with the user's package manager, then playwright install chromium.
If next-browser is already installed, it may be outdated. Run next-browser --version and compare against the latest on npm (npm view @vercel/next-browser version). If the installed version is behind, upgrade it (npm install -g @vercel/next-browser@latest or the equivalent for the user's package manager) before proceeding.
If the project's Next.js version is v16.2.0-canary.37 or later , bundled docs live at node_modules/next/dist/docs/. Before doing PPR work, Cache Components work, or any non-trivial Next.js task, read the relevant doc there — your training data may be outdated. The bundled docs are the source of truth.
See https://nextjs.org/docs/app/guides/ai-agents for background.
open and go.[{"name":"session","value":"..."}], or (2) just "Copy as cURL" from DevTools → Network on any authenticated request — you can extract the cookies from the header yourself.project, config reads) before being asked.screenshot after every navigation, code change, or visual finding. Always caption it (screenshot "Before fix", screenshot "PPR shell — locked"). In headed mode the Screenshot Log window opens automatically so the user sees every screenshot in real time.By default the browser opens headed (visible window). For CI or cloud environments with no display, set NEXT_BROWSER_HEADLESS=1 to run headless.
open <url> [--cookies-json <file>]Launch browser, navigate to URL. With --cookies-json, sets auth cookies before navigating (domain derived from URL hostname).
$ next-browser open http://localhost:3024/vercel --cookies-json cookies.json
opened → http://localhost:3024/vercel (11 cookies for localhost)
Cookie file format: [{"name":"authorization","value":"Bearer ..."}, ...]
Only name and value are required per cookie — omit domain, path, expires, etc. To create the file, use Bash (echo '[...]' > /tmp/cookies.json) since the Write tool requires a prior Read.
closeClose browser and kill daemon.
goto <url>Navigate to a URL with a fresh server render. The browser loads a new document — equivalent to typing a URL in the address bar.
$ next-browser goto http://localhost:3024/vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments
push [path]Client-side navigation — the page transitions without a full reload, the way a user clicks a link in the app. Without a path, shows an interactive picker of all links on the current page.
$ next-browser push /vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments
If push fails silently (URL unchanged), the route wasn't prefetched.
backGo back one page in browser history.
reloadReload the current page from the server.
ssr lockBlock external scripts on all subsequent navigations. While locked, every goto, push, back, and reload shows the raw server-rendered HTML without React hydration or client-side JavaScript — what search engines and social crawlers see.
$ next-browser ssr lock
ssr locked — external scripts blocked on all navigations
ssr unlockRe-enable external scripts. The next navigation will load normally with full hydration.
$ next-browser ssr unlock
ssr unlocked — external scripts re-enabled
perf [url]Profile a full page load — reloads the current page (or navigates to a URL) and collects Core Web Vitals and React hydration timing in one pass.
$ next-browser perf http://localhost:3000/dashboard
# Page Load Profile — http://localhost:3000/dashboard
## Core Web Vitals
TTFB 42ms
LCP 1205.3ms (img: /_next/image?url=...)
CLS 0.03
## React Hydration — 65.5ms (466.2ms → 531.7ms)
Hydrated 65.5ms (466.2 → 531.7)
Commit 2.0ms (531.7 → 533.7)
Waiting for Paint 3.0ms (533.7 → 536.7)
Remaining Effects 4.1ms (536.7 → 540.8)
## Hydrated components (42 total, sorted by duration)
DeploymentsProvider 8.3ms
NavigationProvider 5.1ms
...
TTFB — server response time (Navigation Timing API). LCP — when the largest visible element painted, plus what it was. CLS — cumulative layout shift score (lower is better). Hydration — React reconciler phases and per-component cost (requires React profiling build / next dev; production strips console.timeStamp).
Without a URL, reloads the current page. With a URL, navigates there first.
renders startBegin recording React re-renders. Hooks into onCommitFiberRoot to collect raw per-component data: render count, totalTime, selfTime, DOM mutations, change reasons, and FPS.
Survives full-page navigations (goto/reload) and captures mount and hydration renders — no need to start before or after navigation.
$ next-browser renders start
recording renders — interact with the page, then run `renders stop`
renders stop [--json]Stop recording and print a per-component render profile. Raw data — the agent decides what's actionable.
$ next-browser renders stop
# Render Profile — 3.05s recording
# 426 renders (38 mounts + 388 re-renders) across 38 components
# FPS: avg 120, min 106, max 137, drops (<30fps): 0
## Components by total render time
| Component | Insts | Mounts | Re-renders | Total | Self | DOM | Top change reason |
| ---------------------- | ----- | ------ | ---------- | -------- | -------- | ----- | -------------------------- |
| Parent | 1 | 1 | 9 | 5.8ms | 3.4ms | 10/10 | state (hook #0) |
| MemoChild | 3 | 3 | 27 | 2ms | 1.9ms | 30/30 | props.data |
| Router | 1 | 1 | 9 | 6.3ms | — | 0/10 | parent (ErrorBoundaryHandler) |
## Change details (prev → next)
Parent
state (hook #0): 0 → 1
MemoChild
props.data: {value} → {value}
The Change details section shows the actual prev→next values for each change. This makes the data self-contained — you can see that MemoChild gets props.data: {value} → {value} (same shape, new reference — memo defeated) without needing to inspect the component.
With--json, outputs raw structured JSON with full change arrays per component (type, name, prev, next for each render event).
Columns:
Insts — number of unique component instances observed during recordingMounts — how many times an instance mounted (first render, no alternate fiber)Re-renders — update-phase renders (total renders minus mounts)Total — inclusive render time (component + children)Self — exclusive render time (component only, excludes children)DOM — how many renders actually mutated the DOM vs total rendersTop change reason — most frequent trigger for this componentTiming data (Total, Self) requires a React profiling build (next dev). In production builds these columns show — but render counts, DOM mutations, and change reasons are still reported.
Change reasons — what triggered each re-render:
props.<name> — a prop changed by reference, with prev→next valuesstate (hook #N) — a useState/useReducer hook changed, with prev→next valuescontext (<name>) — a specific context changed, with prev→next valuesparent (<name>) — parent component re-rendered, names the parentparent (<name> (mount)) — parent is also mounting (typical during page load, not a leak)mount — first renderFPS — frames per second during recording. drops counts frames below 30fps.
Up to 200 components are tracked. If output exceeds 4 000 chars it is written to a temp file.
restart-serverRestart the Next.js dev server and clear its caches. Forces a clean recompile from scratch.
Last resort. HMR picks up code changes on its own — reach for this only when you have evidence the dev server is wedged (stale output after edits, builds that never finish, errors that don't clear).
Often exits with net::ERR_ABORTED — this is expected (the page detaches during restart). Follow up with goto <url> to re-navigate after the server is back. Don't treat this error as a failure.
ppr lockPrerequisite: PPR requires cacheComponents to be enabled in next.config. Without it the shell won't have pre-rendered content to show.
Freeze dynamic content so you can inspect the static shell — the part of the page that's instantly available before any data loads. After locking:
goto — shows the server-rendered shell with holes where dynamic content would appear.
push — shows what the client already has from prefetching. Requires the current page to already be hydrated (prefetch is client-side), so lock after you've landed on the origin, not before.
$ next-browser ppr lock locked
ppr unlockResume dynamic content and print a shell analysis — which Suspense boundaries were holes in the shell, what blocked them, and which were static. The output can be very large (hundreds of boundaries). Pipe through | head -20 if you only need the summary and dynamic holes.
$ next-browser ppr unlock
unlocked
# PPR Shell Analysis
# 131 boundaries: 3 dynamic holes, 128 static
## Summary
- Top actionable hole: TrackedSuspense — usePathname (client-hook)
- Suggested next step: This route segment is suspending on client hooks. Check loading.tsx first...
- Most common root cause: usePathname (client-hook) affecting 1 boundary
## Quick Reference
| Boundary | Type | Fallback source | Primary blocker | Source | Suggested next step |
| --- | --- | --- | --- | --- | --- |
| TrackedSuspense | component | unknown | usePathname (client-hook) | tracked-suspense.js:6 | Push the hook-using cl... |
| TeamDeploymentsLayout | route-segment | unknown | unknown | layout.tsx:37 | Inspect the nearest us... |
| Next.Metadata | component | unknown | unknown | unknown | No primary blocker was... |
## Detail
TrackedSuspense
rendered by: TrackedSuspense > RootLayout > AppLayout
environments: SSR
TeamDeploymentsLayout
suspenders unknown: thrown Promise (library using throw instead of use())
## Static (pre-rendered in shell)
GeistProvider at .../geist-provider.tsx:80:9
TrackedSuspense at ...
...
The Quick Reference table is the main overview — boundary, blocker, source, and suggested fix at a glance. The Detail section only appears for holes that have extra info (owner chains, environments, secondary blockers) not already in the table.
errors doesn't report while locked. If the shell looks wrong (empty, bailed to CSR), unlock and goto the page normally, then run errors. Don't debug blind under the lock.
Full bailout (scrollHeight = 0). When PPR bails out completely, unlock returns just "unlocked" with no shell analysis — there are no boundaries to report. In this case, unlock, goto the page normally, then use errors and logs to find the root cause.
treeFull React component tree — every component on the page with its hierarchy, like the Components panel in React DevTools.
$ next-browser tree
# React component tree
# Columns: depth id parent name [key=...]
# Use `tree <id>` for props/hooks/state. IDs valid until next navigation.
0 38167 - Root
1 38168 38167 HeadManagerContext.Provider
2 38169 38168 Root
...
224 46375 46374 DeploymentsProvider
226 46506 46376 DeploymentsTable
tree <id>Inspect one component: ancestor path, props, hooks, state, source location (source-mapped to original file).
$ next-browser tree 46375
path: Root > ... > Prerender(TeamDeploymentsPage) > Prerender(FullHeading) > Prerender(TrackedSuspense) > Suspense > DeploymentsProvider
DeploymentsProvider #46375
props:
children: [<Lazy />, <Lazy />, <span />, <Lazy />, <Lazy />]
hooks:
IsMobile: undefined (1 sub)
Router: undefined (2 sub)
DeploymentListScope: undefined (1 sub)
User: undefined (4 sub)
Team: undefined (4 sub)
...
DeploymentsInfinite: undefined (12 sub)
source: app/(dashboard)/[teamSlug]/(team)/~/deployments/_parts/context.tsx:180:10
IDs are valid until navigation. Re-run tree after goto/push.
viewport [WxH]Show or set the browser viewport size. Useful for testing responsive layouts.
$ next-browser viewport
1440x900
$ next-browser viewport 375x812
viewport set to 375x812
Once set, the viewport stays fixed across navigations. window.resizeTo() via eval is a no-op in Playwright — always use this command to change dimensions.
screenshot [caption] [--full-page]Behavioral rules are in Working with the user → Show, don't tell.
Use screenshot only when visual layout matters (CSS, appearance, PPR shell). For page content or deciding what to click, use snapshot.
Captures the viewport (or full scrollable page with --full-page) to a temp PNG file and returns the path. In headed mode, every screenshot is added to the Screenshot Log — a live browser window that accumulates all screenshots taken during the session. In headless mode the log window is skipped.
The optional caption describes the screenshot or the rationale for taking it. Captions appear in the Screenshot Log above each image.
$ next-browser screenshot "Homepage after login"
/tmp/next-browser-1711234567890.png
$ next-browser screenshot "Full page layout" --full-page
/tmp/next-browser-1711234567891.png
snapshotSnapshot the page's accessibility tree — the semantic structure a screen reader sees — with [ref=eN] markers on every interactive element. Use this to discover what's on the page before clicking.
$ next-browser snapshot
- navigation "Main"
- link "Home" [ref=e0]
- link "Dashboard" [ref=e1]
- main
- heading "Settings"
- tablist
- tab "General" [ref=e2] (selected)
- tab "Security" [ref=e3]
- region "Profile"
- textbox "Username" [ref=e4]
- button "Save" [ref=e5]
The tree shows headings, landmarks (navigation, main, region), and state (selected, checked, expanded, disabled) so you understand page layout, not just a flat element list.
Refs are ephemeral — they reset on every snapshot call and are invalid after navigation. Re-run snapshot after goto/push.
click <ref|text|selector>Click an element using real pointer events (pointerdown → mousedown → pointerup → mouseup → click). This works with libraries that ignore synthetic .click() (Radix UI, Headless UI, etc.).
Three ways to target:
| Input | Example | How it resolves |
|---|---|---|
| Ref from tree | click e3 | Looks up role+name from last snapshot |
| Plain text | click "Security" | Playwright text=Security selector |
| Playwright selector | click "#submit-btn" | Used as-is (CSS, role=, etc.) |
Recommended workflow: run snapshot first, then click eN. Refs are the most reliable — they resolve via ARIA role+name, so they work even when elements have no stable CSS selector.
Clicking navigation links can timeout. click on a Next.js <Link> waits for the navigation to settle, which can exceed the command timeout. If click hangs on a nav link, cancel it and use goto <url> instead.
$ next-browser snapshot
- tablist
- tab "General" [ref=e0] (selected)
- tab "Security" [ref=e1]
$ next-browser click e1
clicked
$ next-browser snapshot
- tablist
- tab "General" [ref=e0]
- tab "Security" [ref=e1] (selected)
fill <ref|selector> <value>Fill a text input or textarea. Clears existing content, then types the new value — dispatches all the events React and other frameworks expect.
$ next-browser snapshot
- textbox "Username" [ref=e4]
$ next-browser fill e4 "judegao"
filled
eval [ref] <script> · eval [ref] --file <path> · eval -Run JS in page context. Returns the result as JSON.
With a ref , the script receives the DOM element as its argument — useful for inspecting a snapshot node or bridging to React internals:
$ next-browser eval e0 'el => el.tagName'
"BUTTON"
$ next-browser eval e0 'el => {
const key = Object.keys(el).find(k => k.startsWith("__reactFiber$"));
if (!key) return null;
let fiber = el[key];
while (fiber && typeof fiber.type !== "function") fiber = fiber.return;
return fiber?.type?.displayName || fiber?.type?.name || null;
}'
"LoginButton"
For simple one-liners (no ref), pass the script inline:
$ next-browser eval 'document.title'
"Deployments – Vercel"
$ next-browser eval 'document.querySelectorAll("a[href]").length'
47
For multi-line or quote-heavy scripts , use --file (or -f) to avoid shell quoting issues entirely:
cat > /tmp/nb-eval.js << 'SCRIPT'
(() => {
// your JS here — no shell escaping needed
return someResult;
})()
SCRIPT
next-browser eval --file /tmp/nb-eval.js
You can also pipe via stdin: echo 'document.title' | next-browser eval -
Use this to read the Next.js error overlay (it's in shadow DOM): next-browser eval 'document.querySelector("nextjs-portal")?.shadowRoot?.querySelector("[data-nextjs-dialog]")?.textContent'
eval runs synchronously in page context — top-level await is not supported. Wrap in an async IIFE if you need to await: next-browser eval '(async () => { ... })()'.
errorsBuild and runtime errors for the current page.
$ next-browser errors
{
"configErrors": [],
"sessionErrors": [
{
"url": "/vercel/~/deployments",
"buildError": null,
"runtimeErrors": [
{
"type": "console",
"errorName": "Error",
"message": "Route \"/[teamSlug]/~/deployments\": Uncached data or `connection()` was accessed outside of `<Suspense>`...",
"stack": [
{"file": "app/(dashboard)/.../deployments.tsx", "methodName": "Deployments", "line": 105, "column": 27}
]
}
]
}
]
}
buildError is a compile failure. runtimeErrors has type: "runtime" (React errors) and type: "console" (console.error calls).
logsRecent dev server log output.
$ next-browser logs
{"timestamp":"00:01:55.381","source":"Server","level":"WARN","message":"[browser] navigation-metrics: skeleton visible was already recorded..."}
{"timestamp":"00:01:55.382","source":"Browser","level":"WARN","message":"navigation-metrics: content visible was already recorded..."}
browser-logsBrowser-side console output (console.log, console.warn, console.error, console.info). Captured directly from the page — works with both dev and production builds.
$ next-browser browser-logs
[LOG ] Initializing app
[WARN ] Deprecation: use fetchV2 instead
[ERROR] Failed to load resource: 404
[INFO ] render complete in 42ms
Up to 500 entries are kept; oldest are dropped when the buffer is full. Entries accumulate across navigations within the same browser session. If output exceeds 4 000 chars it is written to a temp file and the path is printed instead.
When to use which:
| Command | Source | Requires dev server |
|---|---|---|
logs | Next.js dev server stdout | Yes |
errors | Build errors + console.error | Yes |
browser-logs | All browser console output | No |
For dev server diagnostics, prefer logs and errors. Use browser-logs when you need general console output or are running a production build.
networkList all network requests since last navigation.
$ next-browser network
# Network requests since last navigation
# Columns: idx status method type ms url [next-action=...]
# Use `network <idx>` for headers and body.
0 200 GET document 508ms http://localhost:3024/vercel
1 200 GET font 0ms http://localhost:3024/_next/static/media/797e433ab948586e.p.d2077940.woff2
2 200 GET stylesheet 6ms http://localhost:3024/_next/static/chunks/_a17e2099._.css
3 200 GET fetch 102ms http://localhost:3024/api/v9/projects next-action=abc123def
Server actions show next-action=<id> suffix.
network <idx>Full request/response for one entry. Long bodies spill to temp files.
$ next-browser network 0
GET http://localhost:3024/vercel
type: document 508ms
request headers:
accept: text/html,...
cookie: authorization=Bearer...; isLoggedIn=1; ...
user-agent: Mozilla/5.0 ...
response: 200 OK
response headers:
cache-control: no-cache, must-revalidate
content-encoding: gzip
...
response body:
(8234 bytes written to /tmp/next-browser-12345-0.html)
pageRoute segments for the current URL — which layouts, pages, and boundaries are active.
$ next-browser page
{
"sessions": [
{
"url": "/vercel/~/deployments",
"routerType": "app",
"segments": [
{"path": "app/(dashboard)/[teamSlug]/(team)/~/deployments/layout.tsx", "type": "layout", ...},
{"path": "app/(dashboard)/[teamSlug]/(team)/~/deployments/page.tsx", "type": "page", ...},
{"path": "app/(dashboard)/[teamSlug]/layout.tsx", "type": "layout", ...},
{"path": "app/(dashboard)/layout.tsx", "type": "layout", ...},
{"path": "app/layout.tsx", "type": "layout", ...}
]
}
]
}
projectProject root and dev server URL.
$ next-browser project
{
"projectPath": "/Users/judegao/workspace/repo/front/apps/vercel-site",
"devServerUrl": "http://localhost:3331"
}
routesAll app router routes.
$ next-browser routes
{
"appRouter": [
"/[teamSlug]",
"/[teamSlug]/~/deployments",
"/[teamSlug]/[project]",
"/[teamSlug]/[project]/[id]/logs",
...
]
}
action <id>Inspect a server action by its ID (from next-action header in network list).
When the user says "this page is slow after load", "too many re-renders", "laggy interactions", or "janky" — this is update-phase rendering, not initial load. Use renders to profile it. (For initial load, use perf.)
Workflow:
renders start — begin recording.goto the page (the hook survives navigation and captures mount).push, or just wait if the issue is polling/timers.renders stop — read the raw data.Mounts vs Re-renders — is this component re-rendering after load, or is the count just from mount-time cascading?Insts — is a high render count from many instances or one instance rendering excessively?Self — is this component expensive per-render, or just called too often?DOM — did the renders actually produce visible changes? A component with 100 renders and 0 DOM mutations is doing purely wasted work.Verify the fix. After editing the code, HMR picks it up. Re-run renders start / renders stop and compare the raw numbers to the previous profile.
Test your hypothesis before proposing a fix. If you suspect a component is the root cause, find evidence — inspect it with tree, read its source, check what's changing via the change reason column. Don't propose changes from a single observation.
The shell is what the user sees the instant they land — before any dynamic data arrives. The measure is the screenshot while locked: does it read as the page itself? A shell can be non-empty and still bad — one Suspense fallback wrapping the whole content area renders something , but it's a monolithic loading state, not the page.
A meaningful shell is the real component tree with small, local fallbacks where data is genuinely pending. Getting there means the composition layer — the layouts and wrappers between those leaf boundaries — can't itself suspend. ppr unlock's Quick Reference table names the primary blocker and source for each hole; the Detail section adds owner chains and secondary blockers. A suspend high in the tree is what collapses everything beneath it into one fallback.
Work it top-down. For the component that's suspending: can the dynamic access move into a child? If yes, move it — this component becomes sync and rejoins the shell. Follow the access down and ask again.
When you reach a component where it can't move any lower, there are two exits — wrap in a Suspense boundary, or cache it for prerender. Both are human calls (see Working with the user → Escalate, don't decide).
Test your hypothesis before proposing a fix. If you suspect a component is the cause, find evidence — check errors, inspect the component with tree, or compare a route where the shell works to one where it doesn't. Don't commit to a root cause or propose changes from a single observation.
There are two shells depending on how the user arrives — establish which one you're optimizing first (see Working with the user → Escalate, don't decide).
Direct load — the PPR shell. Server HTML for a cold hit on the URL. Lock first, then goto the target — the lock suppresses hydration so you see exactly what the server sent. Screenshot once the load settles, then unlock.
Client navigation — the prefetched shell. What the router already holds when a link is clicked. The origin page decides this — it's the one doing the prefetching — so goto the origin unlocked and let it fully hydrate. Then lock, push to the target, let the navigation settle, screenshot, unlock. Locking before the origin hydrates means nothing got prefetched and push has nothing to show.
Between iterations: check errors while unlocked.
After making a code change: HMR picks it up — just re-lock, goto the page, and re-test. No need to restart-server.
Weekly Installs
1.1K
Repository
GitHub Stars
83
First Seen
Mar 6, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykFail
Installed on
opencode1.0K
codex1.0K
github-copilot1.0K
gemini-cli1.0K
cursor1.0K
cline1.0K
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
Total vs Self — is the cost in this component or its children?parent (X (mount)) is load-time cascading, not a leak.tree to find the component's ID, then tree <id> for its source file, props, and hooks.