Puppeteer截图文本缺失问题排查求助
问题背景
我最近碰到一个非常诡异的问题:用Puppeteer把Express生成的带本地CSS的HTML转成截图时,截图里的文本居然凭空消失了!但直接在本地Chrome里打开这段HTML,所有文本和元素都显示正常;如果移除本地样式,Puppeteer的截图又完全正常。更奇怪的是,偶尔不改代码也能生成正常截图,这让我怀疑是不是存在未处理的竞态条件,但一开始完全摸不着头绪。
重现环境
- Puppeteer版本:0.12.0
- 运行平台:Docker/Ubuntu
- Node.js版本:8
相关代码片段
Express路由代码(index.js)
app.post('/img', function (req, res) { const puppeteer = require('puppeteer'); let css = []; let stylesheets = []; // 分离本地样式表并读取文件内容 css = req.body.stylesheets.filter(sheet => { return sheet.indexOf('https') === -1 && sheet.indexOf('http') === -1; }); css = css.map(sheet => { return fs.readFileSync(path.join(__dirname, sheet)); }); // 分离外部样式表(如bootstrap等) stylesheets = req.body.stylesheets.filter(sheet => { return sheet.indexOf('https') > -1 || sheet.indexOf('http') > -1; }); // 用HTML和样式编译模板 app.render('img', { stylesheets: stylesheets, content: req.body.content, css: css }, function (err, html) { console.log('html\n', html); (async() => { const browser = await puppeteer.launch({args: ['--no-sandbox']}); const page = await browser.newPage(); await page.setViewport({width: 1300, height: 1200}); // 将HTML加载至Chrome try { const loaded = page.waitForNavigation({ waitUntil: 'load' }); await page.setContent(html); await loaded } catch(err) { console.log(err); res.status(err.status).send('There was an error loading the page.'); } // 保存图片 const filename = `${req.body.title}.png`; const filepath = path.join(__dirname, 'img', filename); try { await page.screenshot({ path: filepath }); console.log(`${filename} saved`); } catch(err) { console.log(err); res.status(err.status).send('There was a problem saving the image.'); } res.status(201).send(filename); })(); }); });
HTML模板(img.html)
<!DOCTYPE html> <html> <head> <title>{{title}}</title> {{#stylesheets}} <link rel="stylesheet" type="text/css" href="{{{.}}}"> {{/stylesheets}} {{#css}} <style type="text/css"> {{{.}}} </style> {{/css}} </head> <body> {{{content}}} </body> </html>
问题排查与原因分析
经过反复测试,我发现核心问题出在页面加载的等待逻辑上:
原来的代码中,用page.waitForNavigation配合page.setContent的写法是错误的——page.setContent是直接将HTML内容注入到当前页面,并不会触发浏览器的导航事件,这就导致loaded这个Promise一直处于pending状态,后续的截图操作会跳过等待直接执行。这时候页面可能还在解析本地CSS、渲染元素,尤其是文本样式还没完全应用,就被截图了,所以出现文本缺失的情况。而偶尔的正常截图,完全是因为页面渲染速度刚好赶在了截图操作之前,属于随机事件。
另外,还有一个潜在问题:fs.readFileSync读取本地CSS时没有指定编码格式,可能导致CSS内容出现乱码,进而影响样式解析,加重渲染异常的情况。
解决方案
针对这些问题,我做了三处关键修改:
1. 修正页面加载的等待逻辑
把原来错误的waitForNavigation写法,改成等待页面完全渲染完成的正确方式:
try { await page.setContent(html); // 等待页面网络完全空闲(所有资源加载完成),确保样式解析渲染完毕 await page.waitForNavigation({ waitUntil: 'networkidle0' }); // 如果networkidle0太严格,也可以改用等待DOM加载完成+样式就绪: // await page.waitForFunction('document.readyState === "complete"'); } catch(err) { console.log(err); res.status(500).send('There was an error loading the page.'); }
2. 完善本地CSS读取的编码处理
在读取本地CSS文件时,明确指定utf-8编码,避免乱码:
css = css.map(sheet => { return fs.readFileSync(path.join(__dirname, sheet), 'utf8'); });
3. 优化Docker环境下的Puppeteer启动参数
添加--disable-dev-shm-usage参数,避免Docker环境下的共享内存限制导致渲染异常:
const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-dev-shm-usage'] });
验证结果
修改完成后,测试结果完全符合预期:
- 无本地样式:截图显示正常,和之前一致
- 有本地样式:每次都能生成包含完整文本的截图,再也没有出现文本缺失的情况!
内容的提问来源于stack exchange,提问作者ebbishop




