🚀 快速安装

复制以下命令并运行,立即安装此 Skill:

npx skills add https://skills.sh/vercel-labs/next-browser/next-browser

💡 提示:需要 Node.js 和 NPM

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 文档意识

如果项目的 Next.js 版本是 v16.2.0-canary.37 或更高版本,捆绑的文档位于 node_modules/next/dist/docs/。在进行 PPR 工作、缓存组件工作或任何非平凡的 Next.js 任务之前,请先阅读那里的相关文档——您的训练数据可能已过时。捆绑的文档是唯一的事实来源。

有关背景信息,请参阅 https://nextjs.org/docs/app/guides/ai-agents


当此技能加载时

您的第一条消息应介绍该工具并提出设置问题。不要说“准备好了,你想做什么?”也不要运行推测性命令或自动发现(端口扫描、project、配置读取)。

如果用户已经在他们的消息中提供了 URL、cookie 和任务,请跳过提问——直接执行 open 并开始工作。仅询问缺失的信息。

否则,可以这样说:

这将打开一个带界面的浏览器,连接到您的 Next.js 开发服务器,以便我可以读取 React 组件树、查看 PPR 外壳,并以您在 DevTools 中相同的方式检查错误。开始前:

  • 您的开发服务器 URL 是什么?(它正在运行吗?)
  • 您正在调试的页面是否需要登录?如果需要,我需要您的会话 cookie——最简单的方法是从浏览器的 DevTools → 应用程序 → Cookie 中将它们复制到一个 JSON 文件中,格式如 [{"name":"session","value":"..."}]。如果页面是公开的,请跳过此步骤。

等待回答。然后执行 open <url> [--cookies-json <文件>]。没有打开的会话,其他所有命令都会报错。


命令

open <url> [--cookies-json <文件>]

启动浏览器,导航到 URL。使用 --cookies-json 时,会在导航前设置身份验证 cookie(域名从 URL 主机名派生)。

$ next-browser open http://localhost:3024/vercel --cookies-json cookies.json
已打开 → http://localhost:3024/vercel (为 localhost 设置了 11 个 cookie)

Cookie 文件格式:[{"name":"authorization","value":"Bearer ..."}, ...]

每个 cookie 仅需要 namevalue——省略 domainpathexpires 等。要创建该文件,请使用 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 [路径]

客户端导航——页面无需完全重新加载即可转换,就像用户点击应用中的链接一样。不提供路径时,显示当前页面上所有链接的交互式选择器。

$ next-browser push /vercel/~/deployments
→ http://localhost:3024/vercel/~/deployments

如果推送静默失败(URL 未变),则该路由未被预取。

back

在浏览器历史中后退一页。

reload

从服务器重新加载当前页面。

ssr-goto <url>

在运行任何客户端 JavaScript 之前,准确查看服务器发送的内容。对于验证 SSR 内容、检查搜索引擎和社交爬虫看到的内容、调试水合不匹配以及确认数据出现在初始 HTML 中而不是在客户端获取,此命令非常有用。

页面渲染时不进行水合——没有 React,没有客户端路由,没有 fetch 调用。您看到的是原始服务器输出加上 CSS。

$ next-browser ssr-goto http://localhost:3000/dashboard
→ http://localhost:3000/dashboard (已阻止外部脚本)

之后使用 gotoreload 恢复浏览器正常行为。

perf [url]

分析完整页面加载——重新加载当前页面(或导航到 URL)并一次性收集核心网页指标和 React 水合时序。

$ next-browser perf http://localhost:3000/dashboard
# 页面加载分析 — http://localhost:3000/dashboard

## 核心网页指标
  TTFB                   42ms
  LCP               1205.3ms (图片: /_next/image?url=...)
  CLS                    0.03

## React 水合 — 65.5ms (466.2ms → 531.7ms)
  水合耗时                          65.5ms  (466.2 → 531.7)
  提交阶段                            2.0ms  (531.7 → 533.7)
  等待绘制                            3.0ms  (533.7 → 536.7)
  剩余副作用                          4.1ms  (536.7 → 540.8)

## 已水合的组件 (共 42 个,按耗时排序)
  DeploymentsProvider                       8.3ms
  NavigationProvider                        5.1ms
  ...

TTFB — 服务器响应时间(Navigation Timing API)。
LCP — 最大内容元素绘制的时间点及其内容。
CLS — 累积布局偏移分数(越小越好)。
水合 — React 协调器阶段和每个组件的成本(需要 React profiling 构建 / next dev;生产版本会剥离 console.timeStamp)。

不提供 URL 时,重新加载当前页面。提供 URL 时,先导航到该页面。

restart-server

重启 Next.js 开发服务器并清除其缓存。强制从头开始全新编译。

最后的手段。HMR 通常会自行处理代码更改——仅在您有证据表明开发服务器卡住时(编辑后输出过时、构建永不完成、错误未清除)才使用此命令。

通常会以 net::ERR_ABORTED 退出——这是预期的(页面在重启期间断开连接)。服务器恢复后,使用 goto <url> 重新导航。不要将此错误视为失败。


ppr lock

前提条件: PPR 需要在 next.config 中启用 cacheComponents。否则,外壳将没有预渲染内容可显示。

冻结动态内容,以便您可以检查静态外壳——即在任何数据加载之前页面的立即可用部分。锁定后:

  • goto — 显示服务器渲染的外壳,动态内容的位置会留下空洞。
  • push — 显示客户端从预取中已有的内容。这需要当前页面已经水合完成(预取是客户端行为),因此请在您到达源页面之后再锁定,而不是之前。
$ next-browser ppr lock
已锁定

ppr unlock

恢复动态内容并打印外壳分析——哪些 Suspense 边界是外壳中的空洞、什么阻塞了它们、以及哪些是静态的。输出可能非常大(成百上千个边界)。如果您只需要摘要和动态空洞,可以通过管道 | head -20 限制输出。

$ next-browser ppr unlock
已解锁

# PPR 外壳分析
# 131 个边界:3 个动态空洞,128 个静态

## 摘要
- 首要可操作空洞: TrackedSuspense — usePathname (client-hook)
- 建议下一步: 该路由段因客户端钩子而挂起。首先检查 loading.tsx...
- 最常见根本原因: usePathname (client-hook) 影响了 1 个边界

## 快速参考
| 边界名称                   | 类型              | 回退来源 | 主要阻塞者               | 源代码位置                     | 建议下一步                 |
| ---                        | ---               | ---      | ---                       | ---                            | ---                        |
| TrackedSuspense            | 组件               | 未知     | usePathname (client-hook) | tracked-suspense.js:6          | 将使用钩子的组件推入子... |
| TeamDeploymentsLayout      | 路由段             | 未知     | 未知                      | layout.tsx:37                  | 检查最近的 use...          |
| Next.Metadata              | 组件               | 未知     | 未知                      | 未知                            | 未发现主要阻塞者...         |

## 详情
  TrackedSuspense
    渲染路径: TrackedSuspense > RootLayout > AppLayout
    环境: SSR
  TeamDeploymentsLayout
    未知挂起源: 抛出的 Promise (库使用了 throw 而非 use())

## 静态(在外壳中预渲染)
  GeistProvider 位于 .../geist-provider.tsx:80:9
  TrackedSuspense 位于 ...
  ...

快速参考 表格是主要概览——边界、阻塞者、源代码位置和建议修复方案一目了然。详情 部分仅出现在那些表格中尚未包含额外信息(如所有者链、环境、次要阻塞者)的空洞中。

锁定状态下 errors 不报告。 如果外壳看起来不正确(空白,退化为 CSR),请解锁并正常 goto 该页面,然后运行 errors。不要在锁定状态下盲目调试。

完全退出(scrollHeight = 0)。 当 PPR 完全退出时,unlock 仅返回“已解锁”,没有外壳分析——因为没有边界可报告。在这种情况下,解锁,goto 该页面,然后使用 errorslogs 查找根本原因。


tree

完整的 React 组件树——页面上的每个组件及其层级结构,类似于 React DevTools 中的 Components 面板。

$ next-browser tree
# React 组件树
# 列: 深度 id 父id 名称 [key=...]
# 使用 `tree <id>` 查看属性/钩子/状态。ID 在下一次导航前有效。

0 38167 - Root
1 38168 38167 HeadManagerContext.Provider
2 38169 38168 Root
...
224 46375 46374 DeploymentsProvider
226 46506 46376 DeploymentsTable

tree <id>

检查单个组件:祖先路径、属性、钩子、状态、源代码位置(已通过 source map 映射到原始文件)。

$ next-browser tree 46375
路径: Root > ... > Prerender(TeamDeploymentsPage) > Prerender(FullHeading) > Prerender(TrackedSuspense) > Suspense > DeploymentsProvider
DeploymentsProvider #46375
属性:
  children: [<Lazy />, <Lazy />, <span />, <Lazy />, <Lazy />]
钩子:
  IsMobile: undefined (1 个子)
  Router: undefined (2 个子)
  DeploymentListScope: undefined (1 个子)
  User: undefined (4 个子)
  Team: undefined (4 个子)
  ...
  DeploymentsInfinite: undefined (12 个子)
源代码: app/(dashboard)/[teamSlug]/(team)/~/deployments/_parts/context.tsx:180:10

ID 在导航前有效。在 goto/push 后重新运行 tree


viewport [宽x高]

显示或设置浏览器视口大小。对于测试响应式布局非常有用。

$ next-browser viewport
1440x900

$ next-browser viewport 375x812
视口已设置为 375x812

设置后,视口大小在导航期间保持不变。
在 Playwright 中,通过 eval 调用 window.resizeTo() 是无效操作——始终使用此命令更改尺寸。


screenshot

全页面 PNG 截图,保存到临时文件。返回文件路径。使用 Read 工具读取。

$ next-browser screenshot
/var/folders/.../next-browser-1772770369495.png

不要描述截图显示的内容——用户可以看到浏览器。陈述您的结论或下一步行动,而不是描述页面。

当您需要了解页面内容或决定下一步交互时,优先使用 snapshot 而非 screenshot snapshot 返回结构化的、语义化的数据(角色、名称、状态),您可以直接基于这些数据采取行动——而截图是您需要解释的像素。仅在视觉布局很重要时才使用 screenshot(CSS 问题、验证外观、检查 PPR 外壳)。

snapshot

捕获页面的无障碍树——屏幕阅读器看到的语义结构——并在每个可交互元素上附加 [ref=eN] 标记。使用此命令在点击前发现页面上有什么。

$ next-browser snapshot
- navigation "主导航"
  - link "首页" [ref=e0]
  - link "仪表板" [ref=e1]
- main
  - heading "设置"
  - tablist
    - tab "通用" [ref=e2] (选中)
    - tab "安全" [ref=e3]
  - region "个人资料"
    - textbox "用户名" [ref=e4]
    - button "保存" [ref=e5]

该树显示了标题、地标(navigationmainregion)和状态(选中已勾选展开禁用),以便您了解页面布局,而不仅仅是一个扁平的元素列表。

引用是临时的——它们在每次调用 snapshot 时重置,并在导航后失效。在 goto/push 后重新运行 snapshot

click <引用|文本|选择器>

使用真实的指针事件(pointerdown → mousedown → pointerup → mouseup → click)点击元素。这对那些忽略合成 .click() 的库(如 Radix UI、Headless UI 等)有效。

三种定位方式:

输入 示例 解析方式
来自树的引用 click e3 从上次快照中查找角色+名称
纯文本 click "安全" Playwright text=安全 选择器
Playwright 选择器 click "#submit-btn" 按原样使用(CSS、role= 等)

推荐工作流: 先运行 snapshot,然后 click eN
引用是最可靠的——它们通过 ARIA 角色+名称解析,即使元素没有稳定的 CSS 选择器也能工作。

$ next-browser snapshot
- tablist
  - tab "通用" [ref=e0] (选中)
  - tab "安全" [ref=e1]
$ next-browser click e1
已点击
$ next-browser snapshot
- tablist
  - tab "通用" [ref=e0]
  - tab "安全" [ref=e1] (选中)

fill <引用|选择器> <值>

填充文本输入框或文本区域。清除现有内容,然后输入新值——触发 React 和其他框架期望的所有事件。

$ next-browser snapshot
- textbox "用户名" [ref=e4]
$ next-browser fill e4 "judegao"
已填充

eval [引用] <脚本> · eval [引用] --file <路径> · 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'
(() => {
  // 您的 JS 代码在此 — 无需 shell 转义
  return someResult;
})()
SCRIPT
next-browser eval --file /tmp/nb-eval.js

您也可以通过标准输入管道输入:echo 'document.title' | next-browser eval -

使用此命令读取 Next.js 错误覆盖层(它在影子 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": "路由 \"/[teamSlug]/~/deployments\": 未缓存的数据或 `connection()` 在 `` 外部被访问...",
          "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: 骨架屏可见已记录..."}
{"timestamp":"00:01:55.382","source":"Browser","level":"WARN","message":"navigation-metrics: 内容可见已记录..."}

network

列出上次导航以来的所有网络请求。

$ next-browser network
# 上次导航以来的网络请求
# 列: 索引 状态 方法 类型 耗时(ms) URL [next-action=...]
# 使用 `network <索引>` 查看请求头和响应体。

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 <索引>

单个请求的完整请求/响应信息。长内容会转储到临时文件。

$ next-browser network 0
GET http://localhost:3024/vercel
类型: document  耗时 508ms

请求头:
  accept: text/html,...
  cookie: authorization=Bearer...; isLoggedIn=1; ...
  user-agent: Mozilla/5.0 ...

响应: 200 OK
响应头:
  cache-control: no-cache, must-revalidate
  content-encoding: gzip
  ...

响应体:
(8234 字节已写入 /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 头)检查服务器操作。


场景

扩展静态外壳

外壳是用户在到达页面时立即看到的内容——在任何动态数据到达之前。衡量标准是锁定状态下的截图:它是否像页面本身一样可读?外壳可能非空但仍然糟糕——一个包裹整个内容区域的 Suspense 回退渲染了某些东西,但它是一个整体加载状态,而不是页面本身。

有意义的外壳是真实的组件树,在数据真正待定的地方只有小的、局部的回退。达到这一状态意味着组合层——这些叶子边界之间的布局和包装器——本身不能挂起。ppr unlock 的快速参考表格列出了每个空洞的主要阻塞者和源代码位置;详情部分添加了所有者链和次要阻塞者。树高层的一个挂起点正是导致其下所有内容坍缩为一个回退的原因。

从上到下处理。对于正在挂起的组件:动态访问是否可以移到子组件中?如果可以,就移动它——这个组件变为同步并重新加入外壳。沿着访问路径向下移动并再次询问。

当您到达一个无法再向下移动的组件时,有两个出口——这两个都需要人工判断,把问题交给他们:

  • 将其包装在 Suspense 边界中。回退 UI 应类似于内部渲染的内容——一起设计,不要猜测。
  • 缓存它以便在预渲染时可用(缓存组件)。这些数据是否安全缓存——过时性、谁看到它——是他们的决定,不是您的。

在提出修复方案之前,先检验您的假设。 如果您怀疑某个组件是原因,请寻找证据——检查 errors,使用 tree 检查组件,或者将外壳正常工作的路由与不工作的路由进行比较。不要仅凭单次观察就确定根本原因或提出更改建议。

根据用户的进入方式,有两种外壳。它们的观察方式不同,内容也可能不同——在操作浏览器之前,确定您正在优化的是哪一个。如果要求是“让这个页面加载更快”而没有具体说明,请问:是直接访问 URL,还是从另一个页面点击进入(哪个页面)?不要猜测,也不要两者都做。

直接加载——PPR 外壳。 服务器为直接点击 URL 生成的 HTML。先锁定,然后 goto 目标页面——锁定会抑制水合,因此您能看到服务器发送的确切内容。加载完成后截图,然后解锁。

客户端导航——预取外壳。 当点击链接时,路由器已经持有的内容。源页面决定了这一点——它是执行预取的页面——所以先 不锁定goto 源页面,让它完全水合。然后锁定,push 到目标页面,让导航稳定,截图,解锁。在源页面水合之前锁定意味着没有任何东西被预取,push 也就没有内容可显示。

在迭代之间:在解锁状态下检查 errors

进行代码更改后: HMR 会拾取更改——只需重新锁定,goto 页面,然后重新测试。无需 restart-server

📄 原始文档

完整文档(英文):

https://skills.sh/vercel-labs/next-browser/next-browser

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。