ブラウザでの画面表示と印刷の描画差異に関する実践的考察
おごちゃん
タイトルとOGPはChatGPTが考えたものを穏便にしたものです。
ここのところ、ブログも書かずTwitterもあまりせず、ひたすらHieronymusを開発しています。というのは前回と同じです。
元々Hieronymusは「社内で使う会計システム」だったので、「帳票」というものは存在せず、アウトプットはExcelシートを出力するようにしていました。 どうしても「紙」が必要なら、Excelで印刷すれば良いわけです。 会計資料がExcelで出て来るのは、それはそれで便利ですしね。
前はそれで良かったのですが、現在の版には見積書や請求書を出す機能を作りました。 見積書や請求書については元々は「それ用のページ」を作って、ローカルで印刷orPDF化すればいいと思っていて、そう実装しました。
背景
使えるということであれば「それ用のページ」で良いとは言え、会計システムの方には「電子化証憑の管理」という機能がありますので、請求書を発行したら自動的にそっちに登録するようにした方が便利です。 請求書発行をして作ったPDFをあらためて登録するというフローよりは、請求書を発行する時にPDFを生成してそのまま証憑管理に登録してしまって、同じPDFをダウンロードして印刷する方が、面倒が少ないわけです。
そのためにサーバサイドでPDFを作る必要がありました。
Node.jsでPDFを出力する時の第一選択はPuppeteerです。 比較的古くからあり、多くの人が使って来た実績があります。 ネットにも情報が大量にあります。 (PuppeteerはPDFを作るだけではありませんが)
ということで、ほぼ思考停止状態でPuppeteerを使うことにしました。
Puppeteerが使いものにならない
試した構成
- 帳票をSvelteで書く(これはEJSで書いたものを書き換えるだけ)
- SSRでHTML化(ちょっと面倒だけど想定範囲)
- Puppeteerで Chromium を起動して PDF 出力
ある意味この手のアプリでは王道です。 スタイルシートの調整とかは、EJSで書いていた時に済んでいるので、ほぼ右から左にHTML生成までは行けます。
問題は最後のところです。
Puppeteerが使いものにならない
Puppeteerは定番ということでネットに情報が大量にありますし、ChatGPTも「簡単っしょ」みたいなノリで言って来るので油断をしていたのですが、要約すると以下のようなことが起きました。
puppeteer-core
+chromium
の組み合わせが ARM環境でまともに動かないheadless: true
を指定しても ウィンドウが開く(Firefox)--headless
を追加したら launch() が永遠に返ってこない- バイナリの場所を指定しても エラー詳細が出ず、黙って落ちる
- どのオプションをどう足しても、launch() から返ってこない or WS接続できない
出て来るエラーを読む限りでは、多分これはARM環境であることが大きいのではないかと思われます。
Puppeteerはブラウザを要求しますが、このブラウザの挙動を制御するために原則的にブラウザは自前のものを使います。 そのため、Puppeteerはブラウザを同梱(と言うか自分でダウンロードします)しています。 この同梱のブラウザはARM用もあるらしいのですが、その選択の処理がおかしくて謎挙動をしてしまいます。
そこで既にあるブラウザを使おうとするのですが、これはこれで想定の甘いコードのようで、いくら指定しても起動できません。
Chromiumだからダメなのかと思い、Firefoxにも対応しているという言葉を信じてFirefoxを入れて使おうとするのですが、これもダメ。
- dumpio切った
- DISPLAY消した
--disable-gpu
、--no-zygote
追加- Firefoxバイナリも試した(
PUPPETEER_PRODUCT=firefox
) executablePath
指定- それでも launch() から返ってこない
Puppeteerの開発元の意図としては「公式ビルドのChromium以外は知らん」という姿勢のようです。 ちょっとでも環境がズレるとハマります。
FireFoxをバックエンドに指定した時、launchにheadlessだけじゃなくて、argsにも--headless
とか指定するのですが、launchしてブラウザの画面が出た時は、本当に脱力でした。
これもちょっとだけ調べてみると、どうもバンドル用のFirefoxのheadlessあたりに対応の甘さがあるせいのようです。
うわ〜出た、「**RenderCompositorSWGL failed**」…これは Firefox の **headlessモード + ARM環境 + ソフトウェアレンダリング周りのバグ(というか仕様)」**です。
さらに `/dev/video-*` のエラーもついてて、**完全にグラフィックデバイスがない or アクセス不可な環境**で、Firefoxがフリーズしかけてます。
こちらではどうしようもありません。 いや、OSSなんだから自分でやれよは正論ですけど、ここで頑張る気力も時間も義理もありません。 と言うか、リリースの都合とか考えれば、ここに力を入れてはいけません。
ということで、ここでハマりかけた時にChatGPTが唐突に、
## ✅ 解決策:もうこれしかない
> ✅ **Playwright に切り替える**
とか言って来たので、Playwrightというのを使ってみることにしました。 リポジトリを見ればわかりますが、これはMS製です。
Playwrightに変えたら一瞬だった
そんなわけで、Puppeteerを捨ててPlaywrightを使うことにしてみました。
npm install playwright
npx playwright install chromium
- あとは
page.setContent()
→page.pdf()
で 即、出力成功
↑はChatGPTが言ってることですが、まぁ要するにこれだけです。 悩みどころがまるでありません。 これ以外に書くことがありません。 ブラウザのバイナリがどうこうという悩みは皆無です。 Puppeteerに求めてたのはこれなんだけどなぁ。
Puppeteerとの互換性という点では、ほぼ右から左です。 参考までに、実際に使っているコードを以下に挙げます。
import { chromium } from 'playwright';
const FORM_PATH='../dist-ssr'
/**
* 帳票名に対応するSvelteコンポーネントを動的にインポートしてPDFを生成
* @param {string} reportName - 帳票名(例:'Invoice')
* @param {object} props - 帳票コンポーネントに渡す props(描画データ)
* @returns {Promise<Buffer>} - PDFのバッファ
*/
export const print = async (reportName, props) => {
const reportPath = `${FORM_PATH}/${reportName}.js`;
const { default: ReportComponent } = await import(reportPath);
const origin = `http://localhost:${global.env.port}/`
const { html, head } = ReportComponent.render(props);
let realHead = head.replaceAll(/href="\//g,`href="${origin}`);
const fullHTML = `
<!DOCTYPE html>
<html>
<head>
${realHead}
</head>
<body>${html}</body>
</html>`;
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setContent(fullHTML, {
waitUntil: 'networkidle',
timeout: 10000 // 10秒で打ち切り
});
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true
});
await browser.close();
return pdfBuffer;
}
ぼーっと見るとAPIの名前まで同じに見えるかも知れません。 それくらい似ています。 そして、ハマらない(これ大事)。
🧠 結論:Playwright一択。Puppeteerは思い出の中で死んでてくれ
↑はChatGPTが生成してますw
Puppeteerは、一見枯れているように見えて、利用者も多く、情報も多いという最初の期待に反して、環境差異・ヘッドレス挙動の不安定さ・クロスブラウザ未完成ぶりで酷い目にあいました。
実のところ、数年前からARM環境を主な開発環境にして、それまでは「実績がある」「枯れている」と思っていたものが、「86上のLinux」という環境に依存したことだということがわかって来ました。 Electronも同じようにハマりましたし、ESP-IDFも随分とハマりました。
Playwrightはその点、最初から:
- マルチブラウザ、マルチプラットフォーム対応
- 安定したAPI
- 安心して動くヘッドレスモード
- Dockerとの親和性も◎
というあたりもちゃんとしているようです。
まとめ
2025年現在の開発環境のことを思えば、ヘッドレスブラウザを扱うなら、Playwrightから始めよう。Puppeteerはもう過去の遺物ということなんではないかと思います。
もう少し実のある普遍的で主語が大きい言い方をすれば、86で実績があったからと言ってARMで問題ないとは限らないとも言えます。
以前にも言ってますが、今はあちこちのクラウドがARMを提供していますし、割安な価格設定になっています。 そのことを思えば、「86だけではなくARMでも動く」というのはmustと言っても良いんじゃないかと考えます。 そういった意味で「2025年現在の」という表現にしています。
まぁ今回はChatGPTのアシストがあったので、ハマったのは1日で済んだのですが、自分だけだったら一体どれだけかかったかわかりません。