gpdf vs wkhtmltopdf vs Chromium — Go の PDF 生成 2026
wkhtmltopdf はアーカイブ済み。Chromium は 1 リクエスト 170 MB。gpdf は 13 µs/ページ、ブラウザ不要。2026 年の正直な比較。
TL;DR
wkhtmltopdf は 2023 年 1 月にアーカイブされた。ヘッドレス Chromium (Puppeteer / Playwright / chromedp / go-rod) は動くが、約 170 MB のブラウザバイナリを抱え、同時リクエストごとに 50〜120 MB の RSS を持ち、コールドスタートで 300〜800 ms かかる。gpdf は 13 µs/ページで PDF を生成する。依存ゼロ、ヘッドレスブラウザなし。代償は「任意の HTML+CSS をレンダリングしない」こと、それだけ。
この記事の判断基準: 「デザイナーが Tailwind ページを投げてきて、pixel-perfect に出してほしい」なら Chromium が正解。「請求書、明細、レポート、証明書、ラベル」なら、ネイティブ系は別カテゴリのコスト構造になる。
立場の開示: 私たちは gpdf を作っている側。ベンチマークコードは公開しているし、トレードオフのセクションでは何を諦めたかを明記する。ユースケース表も「gpdf がすべて勝つ」とは書いていない。
3 つのアーキテクチャを並べる
| アプローチ | 代表的ツール | レンダリングエンジン | バイナリサイズ | RSS / リクエスト | コールドスタート | ライセンス |
|---|---|---|---|---|---|---|
| wkhtmltopdf | wkhtmltopdf CLI | QtWebKit fork (〜2014) | 〜40 MB | 〜30〜80 MB | 〜150 ms | LGPLv3 |
| Chromium 系 | Puppeteer / Playwright / chromedp / go-rod | Blink + V8 (本物の Chromium) | 〜170 MB | 〜50〜120 MB | 〜300〜800 ms | BSD + 再配布制約 |
| ネイティブ (gpdf) | gpdf / signintech/gopdf / gofpdf† | 純 Go の PDF Writer | 依存 0 | 〜2〜10 MB | 0 ms | MIT |
† gofpdf と go-pdf/fpdf はどちらもアーカイブ済み。Go 系ライブラリの全体像は2026 年版 Go PDF ライブラリ徹底比較を参照。
説明の前にこの表から読み取れることが 3 つある。
1 つ目。wkhtmltopdf の「バイナリサイズが小さい」は誤解を招く。バイト数が少ないのは、WebKit fork が 10 年以上前から上流追従を止めているから。CVE バックログは小さくない。
2 つ目。Chromium は PDF ライブラリではない。たまたま印刷もできるブラウザだ。その列のコストはすべてブラウザのコスト。
3 つ目。「0 ms vs 300 ms のコールドスタート」は、1 時間に 1 回 PDF を作る長期常駐サーバーにはどうでもいい話。一方で、サーバーレス (Lambda / Cloud Run / Workers) や「1,000 件の PDF を最速で」というバッチでは死活問題になる。
2026 年の wkhtmltopdf
このセクションは読む必要ないかもしれない。既に wkhtmltopdf から離脱しているなら、次のセクションに飛んでください。
そうでない読者へ。wkhtmltopdf の開発は 2022 年に事実上停止し、リポジトリは 2023 年 1 月にアーカイブされた。メンテナの離脱メモは「代替として Chromium を使え」と明記している。理由はインフラ的なものだった。wkhtmltopdf のレンダラーは QtWebKit という WebKit のフォークで、上流の WebKit から 2014 年あたりで分岐したまま。Qt 本体も 2016 年に QtWebKit を非推奨化し、QtWebEngine (Chromium ラッパー) に移行している。wkhtmltopdf が今も使っているフォークは、12 年前のブラウザエンジンだ。
具体的に何が動かないか。モダンな CSS — flex 完全仕様、grid、大量の CSS カスタムプロパティ、aspect-ratio、:has()、container queries、flex の gap、モダンな color 関数 — は誤って描画されるか、まったく描画されない。@font-face の web フォントは大体動くが、可変軸付き web フォントは動かない。SVG サポートは部分的。WOFF2 サポートは遅れて実装され、バグが多い。
したがって 2026 年に「wkhtmltopdf を使う」には 2 つの意味があり、どちらも厳しい。
未パッチの WebKit を含む upstream バージョンに乗っている。 セキュリティチームはいずれ必ずこれを指摘する。「プロジェクトがアーカイブされた」は緩和策ではない。最終リリースは 2020 年。それ以降の CVE 対応は upstream ではなく、Linux ディストロが個別にバックポートしている。
自社フォークを保守している。 Qt と WebKit のソースを読み、パッチをバックポートし、配布する全プラットフォームでリビルドする担当者が必要。実際に見たケースがある。コストは「他のことをしたい」エンジニア 1 名のフルタイムだった。
移行先は Chromium (高い忠実度、高いコスト) かネイティブな PDF ジェネレータ (低コスト、HTML/CSS なし) のどちらか。それがこの記事の残りの話題。
Chromium 系 PDF 生成の実コスト
ヘッドレス Chromium は「本当にブラウザが必要なとき」には正しい道具。コストは 4 か所に現れる。
バイナリ。 Chromium 本体で 〜170 MB。Playwright は既知良好ビルドをバンドル、Puppeteer はインストール時にダウンロード (3 ブラウザ全部入れると 〜280 MB)。コンテナイメージでは最大レイヤーが 1 桁大きくなる。Lambda zip の 250 MB 上限ではこれだけで使い切る。
プロセスあたりメモリ。 起動直後の Chromium プロセスで RSS 〜50 MB。実 CSS、web フォント、画像が数枚あるページを読み込むと 80〜120 MB まで上がる。中身次第で変動するが、下限は変わらない。
コールドスタート。 Chromium を起動して about:blank に飛ばすだけで 〜300 ms (温かいマシンで)。await page.goto(url) + 実ページロード + フォント取得 + await page.pdf() を含めると、初回リクエストでは 500 ms〜2 秒が現実的。プールで温めておけば改善するが、サーバーレスでは効かない。スケールアップごとにコールドスタートを払う。
運用上の表面積。 ブラウザは「自分で決めたつもりのない決定」の大陸だ。CSP をどうするか、networkidle で待つか load で待つか domcontentloaded で待つか、JS を無効化するか、Docker で --disable-dev-shm-usage をどう設定するか、プロセスがリークしたらどうするか。どれも難しくはない。けど全部、本当はやりたくないデバッグ。
正直なカウンター: 忠実度が必要なときは必要だ。デザイナーが Figma エクスポートと Tailwind ページを投げてきて、カスタムフォント、グラデーション、SVG アイコンが「そのまま」出てほしい — これは Chromium の仕事。宣言的なドキュメント API で頑張ると、1 週間溶かしてデザイナーから初回レビューで却下される。
つまり問いは「Chromium を使うか否か」ではない。「自分がレンダリングしているのは本当に web ページか?」だ。
gpdf: ブラウザなしのネイティブレンダリング
gpdf は 3 つ目のカテゴリ — 純 Go の PDF Writer。HTML なし、CSS なし、ヘッドレスブラウザなし。Go (または JSON、または Go テンプレート) でドキュメントを記述すると、PDF バイト列が直接出てくる。
package main
import (
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPaperSize(document.A4),
gpdf.WithMargin(document.Mm(20)),
)
doc.AddPage(func(p *template.PageBuilder) {
p.Row(document.Mm(12), func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("請求書", template.FontSize(24), template.Bold())
})
})
p.Row(document.Mm(8), func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("株式会社 Acme", template.FontSize(11))
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("INV-2026-0517", template.FontSize(11), template.AlignRight())
})
})
// 以降、明細行と合計
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
}
スタック全部。コンテナに Chromium バイナリなし。npm install puppeteer なし。page.goto なし。Write 呼び出しが PDF をライターに直接書き込む。1 ページの請求書ならCPU 時間 〜13 µs。
そのために諦めたもの。レンダラーは display: flex が何を意味するか知らない。知っているのは行、列 (12 カラムグリッド)、テキストラン、画像、テーブル、バーコード。スケールして生成される文書 — 請求書、明細、レシート、レポート、証明書、ラベル、納品書 — のほとんどはこの語彙で足りる。残り (マーケティング PDF、デザイナー主導のブローシャ、もともと Web ページだったもの) では足りない。
パフォーマンス比較
3 カテゴリを比較するのは方法論的に厄介。それぞれ少しずつ違う問題を解いているから。承知の上でやる。公平な比較は「同じ最終成果物、3 つの実装」: ヘッダ + 4×10 の明細表 + 合計の 1 ページ請求書。
| ワークロード | gpdf | wkhtmltopdf (CLI) | Chromium (Playwright page.pdf()) |
|---|---|---|---|
| 1 ページ請求書 | 13 µs | 〜140 ms | 〜280 ms (warm) / 〜1.2 s (cold) |
| 100 ページのページ送りレポート | 683 µs | 〜3.4 s | 〜6.1 s (warm) |
| 1 リクエスト中のピーク RSS | 〜5 MB | 〜70 MB | 〜120 MB |
| コンテナイメージ増加分 | 0 | +40 MB | +170 MB |
Apple M1, Go 1.25 (gpdf 側)、wkhtmltopdf 0.12.6 バイナリ、Playwright 1.42 + 同梱 Chromium。gpdf のベンチマークコードは _benchmark/ — clone して各自のハードウェアで再現可能。
注目すべき数字が 2 つある。
1 ページ請求書の差は約 22,000 倍。大半はレンダリング自体ではなく、リクエストごとにブラウザプロセスを起動・終了させるコスト。Playwright のプールを温めておけば 〜4 倍に縮むが、それでも 4 桁の差。
100 ページレポートの差は約 9,000 倍。ここではレンダリングコストが支配的になり、「ブラウザを起動するコスト」は償却される。償却後も Chromium は要素ごとのレイアウトコストを払う。ネイティブな PDF Writer はそこをスキップする。
本番環境で効いてくるのはピーク RSS の数字だ。Chromium プロセス 1 つが 6 秒間 120 MB を握る = 4 GB のコンテナで同時 30 レポートくらいが上限。同じコンテナで gpdf を走らせると数千同時。
どのアプローチがどこで勝つか
「gpdf が全部勝つ」表ではない。そのつもりもない。実際のアーキテクチャ判断はこういう形になる。
| ユースケース | 正しい道具 | 理由 |
|---|---|---|
| Figma + Tailwind のマーケティング PDF | Chromium (Playwright) | コストよりデザイナー意図への忠実度。 |
| 月末に 50,000 通の明細書 | gpdf | 1 通あたりのコスト × 量 = 実費。CSS は不要。 |
| 単発「デザイナーがブローシャをくれた」 | Chromium (or InDesign) | 量は少なく CSS は多い。1 度きりなら正しい道具を使う。 |
| SaaS 課金システムの請求書 | gpdf | 量が売上に比例。コールドスタートが効く。レイアウトが構造化されている。 |
| 税務書類 / 規制対応申請 (PDF/A) | gpdf (or unidoc) | PDF/A 適合、署名、監査証跡。ブラウザは扱わない。 |
| BI ダッシュボードのスクショ入りレポート | Chromium | チャートが主役。PDF はエクスポート手段。 |
| Markdown を印刷 / ドキュメント PDF | gpdf or Chromium | どちらでも可。コスト vs 忠実度の取引。 |
| レガシー wkhtmltopdf からの移行 | HTML が単純なら gpdf / 実 CSS なら Chromium | テンプレートを先に監査。 |
パターン: 量 × リクエストあたりコスト vs デザイン忠実度。前者が支配的ならネイティブが勝つ。後者が支配的なら Chromium が勝つ。wkhtmltopdf はこの 2026 年のマトリックスのどこにも座る場所がない。
ごまかさないトレードオフ
ずっと匂わせてきたが、節を立てて書く。
gpdf は HTML も CSS もレンダリングしない。既存システムが「HTML メールテンプレートをそのまま PDF にも印刷している」なら、gpdf 移行はそのテンプレートをビルダー API に書き換えること。テンプレ 1 個なら午後一仕事。デザイナーが保守する 30 個のマーケティングテンプレートのライブラリなら、それはプロジェクト。
@font-face の web フォントも扱わない。TTF/OTF ファイルをドキュメント構築時に渡す。CJK フォントは first-class — CGO なしで CJK をレンダリングする話を別記事で書いた — が、フォントファイルを配送するのは開発者の責任。
譲らないもの: 速度、メモリ、デプロイ容易性、依存フットプリント。トレードオフは機能表面積で払っている。本番コストでは払っていない。高ボリュームな構造化ドキュメントを作っているチームの多くは、必要ないブラウザに金を払い続けている、というのが私たちの見立て。そういうチームにはネイティブの道が正解。すべてのチームに gpdf が正解とは思っていない。
コード: 同じ請求書を 3 通り
API の手触りの違いを実感したければ、3 実装を並べる。
Chromium (Playwright, Node):
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const html = fs.readFileSync('invoice.html', 'utf8');
await page.setContent(html, { waitUntil: 'networkidle' });
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '20mm', right: '20mm' },
});
await browser.close();
})();
加えて自分で保守する invoice.html、同梱 Chromium バイナリ (〜170 MB)、フォントの渡し方 (web フォント? base64 埋め込み? --font-render-hinting?)。Tailwind テンプレでは綺麗に動く。保守対象は HTML 側。
wkhtmltopdf (shell):
wkhtmltopdf --enable-local-file-access \
--margin-top 20mm --margin-bottom 20mm --margin-left 20mm --margin-right 20mm \
invoice.html invoice.pdf
加えて wkhtmltopdf バイナリ、QtWebKit-2014 が理解できる CSS だけを使った HTML テンプレ (実用上: grid 不可、flex は要注意、:has() 不可、カスタムプロパティは部分動作)。さらに監査がバイナリを引っかけたときのセキュリティ会話。
gpdf (Go):
doc := gpdf.NewDocument(
gpdf.WithPaperSize(document.A4),
gpdf.WithMargin(document.Mm(20)),
)
doc.AddPage(func(p *template.PageBuilder) {
invoiceHeader(p, "INV-2026-0517", "株式会社 Acme")
invoiceTable(p, lineItems)
invoiceTotals(p, subtotal, tax, total)
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
加えてビルダー API に対して自分で書いた 3 つの Go 関数。テンプレートファイルなし、バイナリ依存なし、別レンダー手順なし。単一の Go バイナリとして FROM scratch コンテナにデプロイできる。
読み方は「どれが一番短いか」ではない。「どの表面積を保守したいか」だ。Chromium の表面積は HTML + CSS + ブラウザ。wkhtmltopdf の表面積は HTML + CSS + 10 年前のブラウザ。gpdf の表面積は Go。
FAQ
2026 年に wkhtmltopdf は本当に使えないのか?
「使えない」は強すぎる。「推奨しない」が正確。動くし、単純なテンプレなら正しい PDF を吐く。新規プロジェクトで採用しない理由: プロジェクトはアーカイブ済み、WebKit fork は 2014 年のコードベース、セキュリティ監査で必ず引っかかる、公式の代替案内が「Chromium を使え」。本番に既に入っているなら移行する時間はある。ただし新規の依存先として今から増やす時間はない。
Chromium のコストを受け入れればいいのでは?
多くのワークロードではそれで OK。上の判断マトリックスでも、マーケティング PDF とデザイナー主導の文書は Chromium 列に置いてある。この記事を書いた理由は、Chromium が「請求書、明細、レポート」 — ブラウザの忠実度が要らないワークロード — にも使われていて、そのコストが AWS の請求書に現れていることが多いから。
Chromium を使わない HTML→PDF (html2pdf や jsPDF) は?
ブラウザ側 JS で HTML を canvas に描いてから PDF にするライブラリ群。忠実度は Chromium より大幅に低い (モダン CSS の多くが動かない)。性能もネイティブより悪い (2 回描画する: HTML → canvas → PDF)。クライアント側 PDF 生成という固有のニッチはあるが、この比較表には載らない。
gpdf は PDF/A や電子署名に対応している?
対応している。PDF/A-1b / PDF/A-2b 適合は gpdf.WithPDFA(...)、PKCS#7 署名 (RFC 3161 タイムスタンプ含む) は gpdf.SignDocument(...)。両方とも MIT コアライブラリに同梱 — 別売りや商用ライセンスは不要。
他の Go の PDF ライブラリ (ブラウザ系ではなく) と比べて?
それは別の問い。短く: gofpdf と go-pdf/fpdf はアーカイブ済み、signintech/gopdf は保守されているが低レベル (レイアウトグリッドなし)、Maroto v2 は保守されているがアーカイブ済み gofpdf の上、unidoc は商用。完全な比較は2026 年版 Go PDF ライブラリ徹底比較。
gpdf を使ってみる
gpdf は Go の PDF 生成ライブラリ。MIT、ゼロ依存、CJK 対応。
go get github.com/gpdf-dev/gpdf
⭐ GitHub で Star する · ドキュメントを読む
関連記事
- なぜ gpdf は他の Go PDF ライブラリの 10〜30 倍速いのか — この記事の数字の裏側にあるアーキテクチャ
- 2026 年版 Go PDF ライブラリ徹底比較 — Go ネイティブライブラリ同士の比較
- gofpdf から gpdf への移行ガイド — アーカイブ済みライブラリから抜けるなら