返回博客

Turbopack dev 首次截图全白:不是渲染门控,是冷编译时序坑

用无头浏览器(CDP / Playwright)对 next dev(turbopack)做自动截图时,偶尔拿到一张纯白图——左下角只有 Next.js 的那个「N」指示圆点,body 里空空荡荡。手动打开同一个 URL,一切正常。更诡异的是:同一轮跑下来,有些路由截出了完整 UI,另一些却是白屏,两者的报错数量都是零。

现象

  • body.innerText.length === 0document.querySelectorAll('*').length 只有 3–10(正常应该几百)
  • 控制台零报错,无异常事件
  • curl 同一 URL 返回 HTTP 200,响应体是几千字节的完整 SSR HTML
  • 同一个 dev server 的其他路由(之前访问过的)截图正常
  • 对白屏路由再刷新一次,内容就出来了

根因

Turbopack(以及 webpack 模式的 next dev)采用按需编译策略:路由模块在首次被请求时才触发编译,编译完成前返回的是一个极简 shell。

自动化脚本往往是整轮测试里第一个访问某条路由的客户端——它恰好踩在编译窗口内,页面模块还没编译完,截图就已经打出去了。编译完成("热身")之后,同一路由的所有后续加载都正常。

这是时序问题,不是渲染门控。正因为如此,控制台没有任何错误:服务端已经成功响应(所以 curl 拿到完整 HTML),只是客户端 JS bundle 还未就绪。

坑:把冷编译空白误判为「客户端渲染门控」

看到白屏 + 零报错,很容易联想到 MSW service worker 未就绪、hydration 失败、某个客户端状态机未解锁。但这些假设都会引导你去检查应用逻辑,越查越找不到根因。

真正的判断依据:白屏是否只在首次访问出现、且刷新后消失?curl 能否拿到完整 HTML? 若两者皆是,就是冷编译时序,不是应用 bug。

正解

1. 先热身,再截图

导航到路由后不要立即截图,等待编译完成再 reload 一次:

// 伪代码,适用于 CDP 或 Playwright
await page.goto(url);
await sleep(4500);          // 给 turbopack 编译时间
await page.reload();
await sleep(3000);          // reload 后稳定一下
await page.screenshot();

2. 轮询 DOM 元素数,而不是固定等待

固定 sleep 在慢机器上仍会误判。更健壮的方式是轮询元素数,低于阈值就再 reload:

const count = await page.evaluate(() => document.querySelectorAll('*').length);
if (count < 30) {
  await page.reload();
  await sleep(5000);   // 路由刚才还是冷的,再等一次
}

3. 先挂监听器,再导航

用 CDP 裸驱动时,如果先 Page.navigate 再 attach,早期 console/exception 事件会全部丢失,让真正的错误也看起来像「无报错白屏」。正确顺序:

新开空白 tab → 启用 Runtime/Page/Log 域 → 再 Page.navigate(url)

4. 截图空白 ≠ 页面坏了,要交叉核验

重路由(大 bundle)即使热身后仍可能偶发白屏。遇到这种情况,用多维度证据替代截图结论:

  • HTTP 200 且响应体 > 几 KB SSR HTML
  • tsc --noEmit 通过
  • 真实浏览器打开正常
正解:navigate → wait → reload → 轮询元素数 → 再截图

热身后 querySelectorAll('*').length 从个位数跳到几百,截图内容完整还原。 两步核心动作:第一次 navigate 触发编译,reload 拿编译完的结果

一句话外卖

自动化截图在 next dev 下全白且无报错,先查是否是冷路由——热身之后再截,不要急着找应用 bug。