PlaywrightのPDF生成が重い?APIに移行してコード量80%削減する方法
Playwrightは優れたブラウザ自動化ツールですが、PDF生成に使うと Chromium のライフサイクル管理、ブラウザプール、メモリ制限、バージョン互換性といった課題が発生します。これらはいずれも PDF 生成そのものとは関係のない運用コストです。
この記事では、FUNBREW PDF API への移行方法を Node.js・Python の Before/After コード例、コスト比較、ステップバイステップの移行手順とともに解説します。
PlaywrightでPDF生成を運用する際の課題
PlaywrightはPuppeteerより高機能ですが、PDF生成用途では同じ運用コストが発生します。
ブラウザコンテキストごとのメモリ消費
Playwright の Chromium 起動は 1 インスタンスあたり 200〜500MB のRAMを消費します。3 件同時生成でそれだけで 600MB〜1.5GB を占有します。アプリケーション本体のメモリを加えると、メモリ制約のあるサーバーでは OOM によるクラッシュが現実的なリスクになります。
コンテキストのリーク管理
Playwright ではブラウザインスタンスとブラウザコンテキストの両方を管理する必要があります。エラー発生時にコンテキストが閉じられないと、ゾンビとしてメモリを占有し続けます。
// よくある問題: エラー時にコンテキストが閉じられない
const context = await browser.newContext();
const page = await context.newPage();
await page.setContent(html); // ここでエラーが発生するとコンテキストが残る
const pdf = await page.pdf();
await context.close(); // エラー時には実行されない
Chromiumのバージョン管理
@playwright/test の npm install ごとに特定の Chromium ビルドにロックされます。アップデートにより、フォントメトリクス、CSS @page サポート、flexbox の挙動が変わり、既存のテストが突然壊れます。ネットワーク制限のある環境では Playwright の Chromium ダウンロードが失敗し、CI/CD パイプラインが止まることもあります。
サーバーレスでのコールドスタート
ウォームな状態でも playwright.chromium.launch() には 1〜3 秒かかります。Lambda や Cloud Run のコールドスタートでは 8〜15 秒に達することがあります。マネージドAPIへの移行でコールドスタートが完全に解消される理由についてはサーバーレス向けPDF API活用ガイドを参照してください。
Before / After:Node.js コード比較
Before: Playwright(自前管理)
const { chromium } = require('playwright');
// ブラウザをリクエスト間で共有するための管理が必要
let browser;
async function getBrowser() {
if (!browser || !browser.isConnected()) {
browser = await chromium.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
}
return browser;
}
async function generatePdfWithPlaywright(html) {
const browser = await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.setContent(html, { waitUntil: 'networkidle' });
const pdf = await page.pdf({
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
});
return pdf;
} finally {
await context.close(); // リークを防ぐために finally で必ず閉じる
}
}
// 他にも必要なもの:
// - ブラウザのヘルスチェックと再起動ロジック
// - 並行処理の制限(セマフォ or キュー)
// - ページごとのタイムアウト処理
// - npx playwright install chromium(数百MBのダウンロード、環境ごとに必要)
// - @playwright/test アップデートごとの動作確認
After: マネージドAPI
async function generatePdf(html) {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/generate', {
method: 'POST',
headers: {
'X-API-Key': process.env.FUNBREW_PDF_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
},
}),
});
if (!response.ok) throw new Error(`PDF API error: ${response.status}`);
return Buffer.from(await response.arrayBuffer());
}
// これだけで完成です。
// ブラウザ管理・コンテキスト・プール・Chromiumインストール、すべて不要。
Playwright版はPDFエンドポイントごとに約40行のインフラ管理コードが必要です。API版は12行で、どこでも再利用できます。
Before / After:Python コード比較
Before: Playwright(Python非同期版)
import asyncio
from playwright.async_api import async_playwright
async def generate_pdf_playwright(html: str) -> bytes:
async with async_playwright() as p:
# Chromiumの起動に1〜3秒かかる(コールドスタート時)
browser = await p.chromium.launch()
context = await browser.new_context()
page = await context.new_page()
try:
await page.set_content(html, wait_until="networkidle")
pdf = await page.pdf(
format="A4",
margin={"top": "20mm", "bottom": "20mm",
"left": "15mm", "right": "15mm"},
print_background=True,
)
finally:
await browser.close()
return pdf
# 必要なもの:
# pip install playwright
# playwright install chromium # 環境ごとに〜300MBのダウンロード
After: マネージドAPI(Python)
import os
import requests
def generate_pdf(html: str) -> bytes:
resp = requests.post(
"https://pdf.funbrew.cloud/api/v1/pdf/generate",
headers={
"X-API-Key": os.environ["FUNBREW_PDF_API_KEY"],
"Content-Type": "application/json",
},
json={
"html": html,
"options": {
"format": "A4",
"margin": {
"top": "20mm", "bottom": "20mm",
"left": "15mm", "right": "15mm",
},
"printBackground": True,
},
},
timeout=30,
)
resp.raise_for_status()
return resp.content
FastAPI・Starlette・Django Channels など非同期Pythonアプリには httpx を使います。
import os
import httpx
async def generate_pdf_async(html: str) -> bytes:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
"https://pdf.funbrew.cloud/api/v1/pdf/generate",
headers={
"X-API-Key": os.environ["FUNBREW_PDF_API_KEY"],
"Content-Type": "application/json",
},
json={
"html": html,
"options": {
"format": "A4",
"margin": {
"top": "20mm", "bottom": "20mm",
"left": "15mm", "right": "15mm",
},
"printBackground": True,
},
},
)
response.raise_for_status()
return response.content
パフォーマンス比較
実測値の目安です(環境・テンプレートの複雑さにより変動します)。
レスポンスタイム
| シナリオ | Playwright(コールドスタート) | Playwright(ウォームプール) | FUNBREW PDF API |
|---|---|---|---|
| シンプルなHTML(〜10KB) | 3,000〜5,000ms | 500〜800ms | 300〜600ms |
| 画像入りHTML(〜100KB) | 4,000〜8,000ms | 800〜1,500ms | 500〜1,200ms |
| 複雑なCSS + JS グラフ | 6,000〜15,000ms | 1,500〜3,000ms | 800〜2,000ms |
| Lambdaコールドスタート | 8,000〜15,000ms | — | 300〜600ms |
メモリ使用量
Playwright(3プロセスプール、Node.js):
アイドル時: 600MB〜1.5GB
PDF生成中: 1GB〜2GB+
10件同時処理時: 3GB+
FUNBREW PDF APIクライアント:
アイドル時: <10MB
PDF生成中: <50MB(HTTPレスポンスのバッファリングのみ)
100件同時処理時: ほぼ同じ(ローカルにChromiumなし)
Dockerイメージサイズ
# Before: Node.js + Playwright + Chromium
FROM node:20
RUN npx playwright install chromium # 〜300MBのダウンロード
# 合計: 900MB以上
# After: APIクライアントのみ
FROM node:20-alpine
# Chromium不要
# 合計: 〜150MB
イメージの軽量化でCIビルド時間短縮・デプロイ高速化・コンテナレジストリのストレージ削減が見込めます。コンテナ環境での詳細はDocker・Kubernetes向けPDF API運用ガイドを参照してください。
コスト比較:セルフホスト vs マネージドAPI
セルフホストのコスト内訳
サーバー費用
Playwright 3プロセスプール(AWS t3.medium: 2vCPU / 4GB RAM):
オンデマンド: 約 $33/月
安全のため4GB以上推奨: t3.large に升格 = 約 $66/月
開発・メンテナンス工数
初期実装(プール + リトライ + タイムアウト): 8〜16時間
Playwright/Chromiumのバージョンアップ対応: 2〜4時間 × 年3〜4回
障害対応(OOM、ゾンビコンテキスト): 2〜8時間 × 月0〜2回
年間メンテナンスコスト試算: エンジニア時間 20〜50時間
CI/CDのオーバーヘッド
Playwright Chromiumのダウンロードは毎CIラン2〜5分の追加が発生します。900MB以上のDockerイメージはプッシュ・プルが遅く、レジストリストレージコストも増加します。
FUNBREW PDF APIのコスト
無料プラン: 月30件まで無料
有料プラン: 件数に応じた従量課金(/pricing参照)
メンテナンス費: ほぼゼロ(HTTPクライアントコードのみ)
損益分岐点の目安
| 月間件数 | セルフホスト | FUNBREW PDF API | 判定 |
|---|---|---|---|
| 〜100件 | $10〜30 + エンジニア工数 | 無料〜低コスト | APIが明らかに有利 |
| 100〜10,000件 | $30〜100 + メンテ工数 | 従量課金 | 工数込みでAPIが有利なことが多い |
| 10,000件以上 | 専用インフラ + 運用チーム | API側でスケール | 要件次第 |
コスト計算で見落とされやすい変数は「エンジニアの機会コスト」です。Chromium管理に使った時間は、収益につながる機能開発には使えません。
移行手順
Step 1: 出力品質の確認
既存のHTMLテンプレートをPlaygroundに貼り付けて、Playwright での出力と並べて比較します。どちらも Chromium ベースのため、ほとんどのケースで出力はほぼ同一です。
Step 2: APIキーの取得
# .env に追加
FUNBREW_PDF_API_KEY=your_key_here
無料プラン(月30件)でAPIキーを取得し、移行前に動作確認できます。
Step 3: PDF生成関数の差し替え
既存の実装と同じシグネチャのラッパー関数を作ります。
// utils/pdf.js — Playwright ベースの generatePdf() のドロップイン置き換え
export async function generatePdf(html, options = {}) {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/generate', {
method: 'POST',
headers: {
'X-API-Key': process.env.FUNBREW_PDF_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
...options,
},
}),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`PDF API error ${response.status}: ${body}`);
}
return Buffer.from(await response.arrayBuffer());
}
既存の呼び出し箇所(const pdf = await generatePdf(html))はすべて変更不要です。
Step 4: CSSメディアクエリの確認
Playwright のデフォルトは screen メディアタイプ、API のデフォルトは print です。@media screen のスタイルを確認して変換します。
/* Before: screen限定(APIのprintメディアタイプでは無視される) */
@media screen {
.invoice { padding: 40px; background: #fff; }
}
/* After: 無条件に適用するか、print向けに明示 */
.invoice { padding: 40px; background: #fff; }
/* または */
@media print {
.invoice { padding: 40px; background: #fff; }
}
Step 5: SSR依存パターンの変換
page.waitForSelector() や page.evaluate() でデータを注入していた場合はサーバーサイドに移動します。
// Before: PlaywrightでのDOM操作
await page.evaluate((data) => {
document.querySelector('#total').textContent = data.total;
document.querySelector('#customer').textContent = data.customerName;
}, invoiceData);
const pdf = await page.pdf();
// After: サーバーサイドでHTMLを生成してからAPI呼び出し
const html = renderInvoiceTemplate(invoiceData); // 完全なHTMLを先に生成
const pdf = await generatePdf(html); // APIに渡す
Step 6: Playwrightの依存を削除
npm uninstall playwright @playwright/test
# Dockerfileから削除:
# RUN npx playwright install chromium
# CI/CDから削除:
# - Playwright Chromiumのキャッシュステップ
# - PLAYWRIGHT_BROWSERS_PATH 環境変数
移行後の本番運用チェックリストはPDF API本番運用チェックリストを参照してください。
移行時によくある問題と解決策
問題1: 外部画像が表示されない
原因: APIサーバーが localhost やプライベートネットワーク上のURLにアクセスできない。
解決策: 画像を Base64 データURIに変換してHTMLに埋め込みます。
import fs from 'fs';
import path from 'path';
function embedImage(imagePath) {
const ext = path.extname(imagePath).slice(1);
const data = fs.readFileSync(imagePath).toString('base64');
return `data:image/${ext};base64,${data}`;
}
const html = `<img src="${embedImage('./logo.png')}" alt="ロゴ">`;
問題2: Google Fontsが読み込まれない
原因: ネットワークレイテンシまたはアウトバウンドアクセス制限によりフォントリクエストがタイムアウトする。
解決策: waitForNetworkIdle: true(デフォルト有効)を使うか、フォントをBase64でHTMLに埋め込みます。日本語テキストについては FUNBREW PDF に Noto Sans JP がプリインストールされているため追加対応は不要です。
<style>
/* Google Fonts URL(外部アクセスが可能な場合) */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap');
/* または確実に動作するBase64埋め込み */
@font-face {
font-family: 'MyFont';
src: url('data:font/woff2;base64,d09GMgAB...') format('woff2');
}
body { font-family: 'Noto Sans JP', sans-serif; }
</style>
問題3: Chart.jsのグラフが空白になる
原因: PDFが生成される時点でグラフアニメーションがまだ実行中。
解決策: animation: false を設定し、CDNからChart.jsを読み込む場合は waitForNetworkIdle: true を指定します。
new Chart(ctx, {
type: 'bar',
data: { ... },
options: {
animation: false, // PDF生成時は必須
responsive: true,
},
});
Chart.js + PDF の完全なパターンはPDFレポート自動生成ガイドを参照してください。
問題4: タイムアウトエラーが発生する
解決策: 指数バックオフ付きのリトライロジックを実装します。
async function generatePdfWithRetry(html, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30_000); // 30秒
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/generate', {
method: 'POST',
headers: {
'X-API-Key': process.env.FUNBREW_PDF_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, options }),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.status === 429 && attempt < maxRetries) {
// レート制限: 指数バックオフで待機してリトライ
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
continue;
}
if (!response.ok) throw new Error(`API error: ${response.status}`);
return Buffer.from(await response.arrayBuffer());
} catch (err) {
if (attempt === maxRetries) throw err;
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}
完全なリトライ・アラートパターンはPDF APIエラーハンドリング完全ガイドを参照してください。
移行チェックリスト
移行前
[ ] Playground でHTMLを貼り付けて Playwright の出力と比較
[ ] 無料プランに登録し FUNBREW_PDF_API_KEY を取得
[ ] generatePdf(html, options) ラッパー関数を作成
[ ] ステージング環境でPDF出力を目視比較
CSS確認
[ ] @media screen / @media print の使い分けを確認
[ ] page-break-before / break-before: page の動作確認
[ ] 外部フォントを @font-face Base64 に変換済みか確認
[ ] printBackground: true で背景色・背景画像が表示されるか確認
コード確認
[ ] page.waitForSelector() 呼び出しをSSRに変換
[ ] page.evaluate() DOM操作をテンプレートレンダリングに移行
[ ] ローカル画像パスを Base64 に変換済みか確認
本番移行
[ ] エラーハンドリング(429はリトライ、5xxはアラート)を実装
[ ] クライアントタイムアウトを30秒に設定
[ ] npm uninstall playwright @playwright/test を実行
[ ] Dockerfileから npx playwright install を削除
[ ] CI/CDのPlaywrightキャッシュを削除
[ ] サーバーのメモリ割り当てを見直し(Chromium不要になるため削減可能)
移行による改善ポイント
| 項目 | Playwright(自前管理) | FUNBREW PDF API |
|---|---|---|
| メモリ使用量 | 200〜500MB/コンテキスト | ほぼゼロ(HTTPクライアントのみ) |
| コールドスタート | 1〜3秒 | なし |
| 並行処理 | 手動のコンテキストプール管理 | API側で自動スケーリング |
| Chromiumバージョン管理 | アップデートごとに対応必要 | 不要 |
| サーバーレス(Lambda/Cloud Run) | レイヤー等の工夫が必要 | そのまま動作 |
| Dockerイメージサイズ | 900MB以上 | 〜150MB |
| Lambdaコールドスタート | 8〜15秒 | 300〜600ms |
| CIビルド時間 | +2〜5分(Chromiumダウンロード) | 変化なし |
まず試してみる
Playgroundで既存のHTMLテンプレートを貼り付けるだけでPDF出力をすぐに確認できます(登録不要)。コードでテストする場合は無料プラン(月30件)でAPIキーを取得してステージング環境で動作確認できます。
APIの詳細仕様はドキュメントを参照してください。
関連リンク
- PuppeteerからのPDF API移行ガイド — Puppeteer固有の移行コンテキスト
- PDFレポート自動生成ガイド — Chart.js・cron・メール送信の実装パターン
- PDF生成APIクイックスタート — Node.js・Python・PHPのコード例
- HTML to PDF CSSトラブルシューティング — CSS互換性とprintメディアタイプの対処法
- サーバーレス向けPDF API活用ガイド — Lambda・Cloud Run対応
- PDF APIエラーハンドリング完全ガイド — リトライロジックとアラートのパターン
- PDF API本番運用チェックリスト — 移行後の本番運用全般
- Docker・Kubernetes向けPDF API運用ガイド — コンテナ環境でのデプロイ
- APIリファレンス — エンドポイントの詳細仕様