Turbopack dev 首次截图全白:不是渲染门控,是冷编译时序坑
用无头浏览器(CDP / Playwright)对 next dev(turbopack)做自动截图时,偶尔拿到一张纯白图——左下角只有 Next.js 的那个「N」指示圆点,body 里空空荡荡。手动打开同一个 URL,一切正常。更诡异的是:同一轮跑下来,有些路由截出了完整 UI,另一些却是白屏,两者的报错数量都是零。
现象
body.innerText.length === 0,document.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通过- 真实浏览器打开正常
热身后 querySelectorAll('*').length 从个位数跳到几百,截图内容完整还原。
两步核心动作:第一次 navigate 触发编译,reload 拿编译完的结果。
一句话外卖
自动化截图在
next dev下全白且无报错,先查是否是冷路由——热身之后再截,不要急着找应用 bug。