記事一覧

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 かかる。gpdf13 µs/ページで PDF を生成する。依存ゼロ、ヘッドレスブラウザなし。代償は「任意の HTML+CSS をレンダリングしない」こと、それだけ。

この記事の判断基準: 「デザイナーが Tailwind ページを投げてきて、pixel-perfect に出してほしい」なら Chromium が正解。「請求書、明細、レポート、証明書、ラベル」なら、ネイティブ系は別カテゴリのコスト構造になる。

立場の開示: 私たちは gpdf を作っている側。ベンチマークコードは公開しているし、トレードオフのセクションでは何を諦めたかを明記する。ユースケース表も「gpdf がすべて勝つ」とは書いていない。

3 つのアーキテクチャを並べる

アプローチ代表的ツールレンダリングエンジンバイナリサイズRSS / リクエストコールドスタートライセンス
wkhtmltopdfwkhtmltopdf CLIQtWebKit fork (〜2014)〜40 MB〜30〜80 MB〜150 msLGPLv3
Chromium 系Puppeteer / Playwright / chromedp / go-rodBlink + V8 (本物の Chromium)〜170 MB〜50〜120 MB〜300〜800 msBSD + 再配布制約
ネイティブ (gpdf)gpdf / signintech/gopdf / gofpdf†純 Go の PDF Writer依存 0〜2〜10 MB0 msMIT

† 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 ページ請求書。

ワークロードgpdfwkhtmltopdf (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 のマーケティング PDFChromium (Playwright)コストよりデザイナー意図への忠実度。
月末に 50,000 通の明細書gpdf1 通あたりのコスト × 量 = 実費。CSS は不要。
単発「デザイナーがブローシャをくれた」Chromium (or InDesign)量は少なく CSS は多い。1 度きりなら正しい道具を使う。
SaaS 課金システムの請求書gpdf量が売上に比例。コールドスタートが効く。レイアウトが構造化されている。
税務書類 / 規制対応申請 (PDF/A)gpdf (or unidoc)PDF/A 適合、署名、監査証跡。ブラウザは扱わない。
BI ダッシュボードのスクショ入りレポートChromiumチャートが主役。PDF はエクスポート手段。
Markdown を印刷 / ドキュメント PDFgpdf 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 する · ドキュメントを読む

関連記事