gpdf vs wkhtmltopdf vs Chromium: generación de PDF en 2026
wkhtmltopdf está archivado. Chromium consume 170 MB por petición. gpdf renderiza una página en 13 µs sin navegador. Comparación honesta para 2026.
TL;DR
wkhtmltopdf fue archivado en enero de 2023. Chromium headless (Puppeteer, Playwright, chromedp, go-rod) funciona, pero arrastra un binario de navegador de ~170 MB, mantiene 50–120 MB residentes por petición concurrente y añade 300–800 ms de arranque en frío. gpdf renderiza una página de PDF en 13 µs, sin dependencias y sin navegador headless — al precio de no renderizar HTML+CSS arbitrarios.
Regla de decisión para el resto del artículo: si tu requisito es "la diseñadora me pasa una página Tailwind y la quiere pixel-perfect", Chromium sigue siendo la herramienta correcta. Si es "factura, extracto, informe, certificado, etiqueta", la vía nativa pertenece a otra categoría de coste.
Aviso de sesgo: nosotros publicamos gpdf. El código de los benchmarks es público, la sección de trade-offs nombra lo que sacrificamos y la matriz de casos no pretende que gpdf gane en todo.
Las tres arquitecturas, una al lado de la otra
| Enfoque | Herramientas representativas | Motor de renderizado | Tamaño binario | RSS / petición | Arranque en frío | Licencia |
|---|---|---|---|---|---|---|
| wkhtmltopdf | wkhtmltopdf CLI | Fork de QtWebKit (~2014) | ~40 MB | ~30–80 MB | ~150 ms | LGPLv3 |
| Basado en Chromium | Puppeteer, Playwright, chromedp, go-rod | Blink + V8 (Chromium real) | ~170 MB | ~50–120 MB | ~300–800 ms | BSD + restricciones de redistribución |
| Nativo (gpdf) | gpdf, signintech/gopdf, gofpdf† | PDF Writer puro en Go | 0 deps | ~2–10 MB | 0 ms | MIT |
† gofpdf y go-pdf/fpdf están ambos archivados; el resto del panorama Go está en nuestra comparativa 2026.
Tres cosas que leer en esta tabla antes de explicarlas.
Uno: la "huella binaria pequeña" de wkhtmltopdf es engañosa. El conteo de bytes es bajo porque su fork de WebKit dejó de seguir el upstream hace más de una década. El backlog de CVEs no es bajo.
Dos: Chromium no es una librería de PDF — es un navegador que también imprime. Cada coste de esa columna es un coste de navegador.
Tres: la diferencia entre "0 ms" y "300 ms" de arranque en frío no es interesante para un servidor de larga duración que genera un PDF por hora. Es existencial para serverless (Lambda, Cloud Run, Workers) y para trabajos por lotes de tipo "1.000 PDFs lo más rápido posible".
wkhtmltopdf en 2026
Puede que no necesites leer esta sección. Si tu equipo ya salió de wkhtmltopdf, salta a la siguiente.
Para los demás: el desarrollo de wkhtmltopdf paró efectivamente en 2022, el repositorio fue archivado en enero de 2023 y la nota de despedida del mantenedor recomienda Chromium como reemplazo. La razón fue de infraestructura. El renderizador de wkhtmltopdf es QtWebKit, un fork de WebKit que dejó de seguir el upstream alrededor de 2014. El propio Qt depreció QtWebKit en 2016 a favor de QtWebEngine (que envuelve Chromium). El fork que wkhtmltopdf todavía usa es un motor de navegador de hace 12 años.
En concreto, el CSS moderno — especificación completa de flex, grid, propiedades personalizadas a gran escala, aspect-ratio, :has(), container queries, gap en flex, funciones de color modernas — o se renderiza mal o no se renderiza. Las web fonts vía @font-face funcionan en general; las web fonts con ejes variables, no. El soporte de SVG es parcial. El soporte de WOFF2 llegó tarde y trae bugs.
Por tanto, "usar wkhtmltopdf" en 2026 tiene dos significados, ambos malos.
Estás en una versión upstream que incluye código WebKit sin parchear. Los equipos de seguridad acabarán marcándolo, y "el proyecto está archivado" no es un plan de remediación. La última release fue en 2020. El trabajo de CVE desde entonces lo han hecho las distros de Linux retroportando parches, no el upstream.
Mantienes un fork privado. Alguien tiene que leer fuente de Qt y de WebKit, retroportar parches y reconstruir para cada plataforma que despliegas. Hemos visto este caso. El coste es un ingeniero a tiempo completo que preferiría estar haciendo otra cosa.
La pregunta de migración es si lo reemplazas con Chromium (alta fidelidad, alto coste) o con un generador de PDF nativo (bajo coste, sin HTML/CSS). De eso va el resto del post.
Generación basada en Chromium: lo que realmente pagas
Chromium headless es la herramienta correcta cuando de verdad necesitas un navegador. El coste aparece en cuatro lugares.
El binario. Chromium mismo pesa ~170 MB. Playwright incluye un build conocido bueno; Puppeteer descarga uno en la instalación (~280 MB sumando los tres navegadores). En una imagen de contenedor, esto será tu capa más grande por un orden de magnitud. En un zip de Lambda con el límite de 250 MB, es prácticamente toda la capa.
Memoria por proceso. Un proceso Chromium recién levantado ocupa ~50 MB residentes. Cargar una página no trivial (CSS real, web fonts, un par de imágenes) lo lleva a 80–120 MB. Los números varían con la página. El suelo no.
Arranque en frío. Lanzar Chromium y navegar a about:blank lleva ~300 ms en una máquina caliente. Añadir await page.goto(url) + carga real de la página + obtención de fuentes + await page.pdf() es más típicamente 500 ms a 2 segundos en la primera petición. Mantener un pool de Chromium caliente ayuda; no ayuda en serverless, donde pagas el arranque en frío en cada evento de escalado.
Superficie operacional. Un navegador es un continente de decisiones que no querías tomar: cómo manejar CSP, si esperar networkidle o load o domcontentloaded, si deshabilitar JS, cómo configurar --disable-dev-shm-usage en Docker, qué hacer cuando el proceso del navegador tiene fugas. Ninguno es difícil. Todos juntos son depuración que preferirías no estar haciendo.
Hay un contraargumento honesto: cuando necesitas la fidelidad, la necesitas. Un PDF de marketing diseñado por alguien que te pasa un export de Figma y una página Tailwind, con fuentes personalizadas, gradientes e iconos SVG que deben renderizarse exactamente — eso es trabajo para Chromium. Intentarlo con una API de documento declarativa te quema una semana y produce algo que la diseñadora rechaza en la primera revisión.
Así que la pregunta no es "¿Chromium sí o no?". Es: "¿lo que estoy renderizando es realmente una página web?".
gpdf: renderizado nativo sin navegador
gpdf está en la tercera categoría — un PDF Writer nativo en Go. Sin HTML, sin CSS, sin navegador headless. Describes el documento en Go (o JSON, o plantillas Go) y la librería emite los bytes de PDF directamente.
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("Factura", template.FontSize(24), template.Bold())
})
})
p.Row(document.Mm(8), func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("Acme S.A.", template.FontSize(11))
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("INV-2026-0517", template.FontSize(11), template.AlignRight())
})
})
// A continuación: líneas y totales
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
}
Esa es la pila completa. Sin binario de Chromium en el contenedor. Sin npm install puppeteer. Sin page.goto. La llamada Write emite el PDF directamente al writer — para una factura de una página, ~13 µs de CPU.
Lo que entregamos a cambio: el renderizador no sabe qué significa display: flex. Sabe filas, columnas (rejilla de 12 columnas), runs de texto, imágenes, tablas, códigos de barras. Para la mayoría de los documentos que se generan a escala — facturas, extractos, recibos, informes, certificados, etiquetas, albaranes — ese vocabulario alcanza. Para el resto (PDFs de marketing, folletos dirigidos por diseño, cualquier cosa que empezó siendo una página web), no.
La comparación de rendimiento
Hacer benchmark de estas tres categorías entre sí es metodológicamente espinoso porque resuelven problemas ligeramente distintos. Lo hacemos igual. La comparación justa es "el mismo producto final, tres implementaciones": una factura de una página con encabezado, tabla 4×10 de líneas y totales.
| Carga | gpdf | wkhtmltopdf (CLI) | Chromium (Playwright page.pdf()) |
|---|---|---|---|
| Factura de una página | 13 µs | ~140 ms | ~280 ms (caliente) / ~1.2 s (frío) |
| Informe paginado de 100 páginas | 683 µs | ~3.4 s | ~6.1 s (caliente) |
| RSS pico durante una sola petición | ~5 MB | ~70 MB | ~120 MB |
| Impacto en el tamaño de imagen | 0 | +40 MB | +170 MB |
Apple M1, Go 1.25 para gpdf; binario wkhtmltopdf 0.12.6; Playwright 1.42 con Chromium incluido. El código del benchmark de gpdf está en _benchmark/ — clónalo y vuélvelo a correr en tu hardware.
Dos números que merecen un párrafo.
La diferencia en la factura de una página es de aproximadamente 22.000×. La mayor parte no es el renderizado en sí — es el coste de arrancar y derribar un proceso de navegador por petición. Si mantienes un pool de Playwright caliente, lo reduces ~4×; sigues estando a cuatro órdenes de magnitud.
La diferencia del informe de 100 páginas es de aproximadamente 9.000×. Aquí el coste de renderizado domina y la sobrecarga constante de "arrancar un navegador" se amortiza. Incluso amortizado, Chromium paga costes de layout por elemento que un PDF writer nativo se salta.
El número que muerde en producción es el del RSS pico. Un proceso Chromium reteniendo ~120 MB durante un trabajo de 6 segundos significa que un contenedor de 4 GB puede manejar unos 30 informes concurrentes. El mismo contenedor corriendo gpdf maneja miles.
Cuándo gana cada enfoque
Esto no es una matriz de "gpdf gana en todo". No debería serlo. Las decisiones de arquitectura reales tienen este aspecto.
| Caso de uso | Herramienta correcta | Por qué |
|---|---|---|
| PDF de marketing desde un diseño Figma + Tailwind | Chromium (Playwright) | La fidelidad a la intención del diseño importa más que el coste. |
| 50.000 extractos mensuales a fin de mes | gpdf | Coste por documento × volumen = dinero real. CSS no es necesario. |
| Único "la diseñadora pidió un folleto" | Chromium (or InDesign) | Volumen bajo, CSS alto. Usar la herramienta adecuada una vez. |
| Facturas para un sistema de facturación SaaS | gpdf | El volumen escala con los ingresos. El frío importa. Layout estructurado. |
| Modelos fiscales / presentaciones regulatorias (PDF/A) | gpdf (o unidoc) | Conformidad PDF/A, firma, rastros de auditoría. Los navegadores no hacen esto. |
| Informe de un dashboard BI con capturas de gráficos | Chromium | El gráfico es el punto. El PDF es la exportación. |
| "Imprimir el Markdown" / PDF de documentación | gpdf o Chromium | Cualquiera vale. Coste vs fidelidad. |
| Migración heredada de wkhtmltopdf | gpdf si el HTML era simple; Chromium si el CSS era serio | Auditar primero las plantillas. |
El patrón: volumen × coste por petición vs fidelidad del diseño. Si domina el primer eje, gana el nativo. Si domina el segundo, gana Chromium. wkhtmltopdf no tiene sitio en esta matriz en 2026.
El trade-off que no fingimos que no existe
Hemos estado dando vueltas a esto. Merece sección propia.
gpdf no renderiza HTML ni CSS. Si tu sistema actual es "tenemos una plantilla de email HTML que también imprimimos a PDF", migrar a gpdf significa reescribir esa plantilla contra la API builder de gpdf. Para una plantilla, una tarde. Para una biblioteca de 30 plantillas de marketing mantenidas por diseño, es un proyecto.
Tampoco renderizamos web fonts vía @font-face. Pasas archivos TTF/OTF a gpdf en el momento de construir el documento. Las fuentes CJK en particular son first-class — escribimos sobre por qué renderizamos CJK sin CGO — pero el desarrollador tiene que distribuir el archivo de fuente.
En lo que no transigimos: velocidad, memoria, facilidad de despliegue, huella de dependencias. El trade-off se paga en superficie de funcionalidades, no en coste de producción. Pensamos que muchos equipos que generan documentos estructurados de alto volumen han estado pagando de más por un navegador que no necesitaban, y la respuesta correcta para esos equipos es la vía nativa. No pensamos que gpdf sea la respuesta correcta para todos los equipos.
Código: la misma factura, tres formas
Si quieres notar de verdad las diferencias de API, aquí están las tres implementaciones lado a lado.
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();
})();
Más invoice.html (que tú mantienes), más el binario Chromium incluido (~170 MB), más una forma de manejar la carga de fuentes (¿web fonts? ¿base64 incrustado? --font-render-hinting?). Funciona precioso en una plantilla Tailwind; la superficie de mantenimiento es el HTML.
wkhtmltopdf (shell):
wkhtmltopdf --enable-local-file-access \
--margin-top 20mm --margin-bottom 20mm --margin-left 20mm --margin-right 20mm \
invoice.html invoice.pdf
Más el binario de wkhtmltopdf, más una plantilla HTML que evite el CSS que QtWebKit-2014 no entiende (en la práctica: nada de grid, cuidado con flex, sin :has(), propiedades personalizadas funcionan a medias). Más la conversación de seguridad cuando el binario sea marcado en una auditoría.
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 S.A.")
invoiceTable(p, lineItems)
invoiceTotals(p, subtotal, tax, total)
})
out, _ := os.Create("invoice.pdf")
defer out.Close()
doc.Write(out)
Más tres funciones Go que escribiste contra la API builder. Sin archivos de plantilla, sin dependencia binaria, sin paso de render separado. Desplegable como un único binario Go en un contenedor FROM scratch.
La forma correcta de leer esto no es "cuál es el más corto". Es: "qué superficie quiero mantener". La superficie de Chromium es HTML + CSS + un navegador; la de wkhtmltopdf es HTML + CSS + un navegador de hace una década; la de gpdf es Go.
FAQ
¿De verdad wkhtmltopdf es inusable en 2026?
Inusable es fuerte. No recomendable es la palabra correcta. Sigue corriendo, sigue produciendo PDFs y para plantillas sencillas produce PDFs correctos. Las razones para no empezar un proyecto nuevo con él: el proyecto está archivado, el fork de WebKit es código de 2014, las auditorías de seguridad lo marcarán, y la guía oficial de reemplazo es "usa Chromium". Si tienes wkhtmltopdf en producción hoy, tienes tiempo para migrar; no tienes tiempo para añadir nuevas dependencias sobre él.
¿No puedo simplemente correr Chromium y aceptar el coste?
Para la mayoría de cargas, sí. La matriz de decisión arriba pone a los PDFs de marketing y a los documentos dirigidos por diseño firmemente en la columna de Chromium. La razón por la que existe este post es que Chromium también se está usando para facturas, extractos e informes — cargas donde el navegador está pagando una fidelidad que el documento no necesita. Ahí es donde el coste aparece en la factura de AWS.
¿Y HTML-a-PDF sin Chromium, como html2pdf o jsPDF?
Esas son librerías de JS en el navegador que renderizan HTML a canvas y de ahí a PDF. La fidelidad es significativamente peor que con Chromium (la mayoría del CSS moderno no funciona) y el rendimiento es peor que nativo (renderizas dos veces: HTML → canvas → PDF). Tienen su nicho — generación de PDF en cliente sin servidor — pero no están en la misma comparación.
¿gpdf soporta PDF/A o firmas digitales?
Sí. gpdf.WithPDFA(...) para conformidad PDF/A-1b y PDF/A-2b, gpdf.SignDocument(...) para firmas PKCS#7 incluido timestamping RFC 3161. Ambos en la librería core MIT — sin add-on, sin licencia comercial.
¿Cómo se compara gpdf con otras librerías de PDF en Go (no navegadores)?
Otra pregunta. Versión corta: gofpdf y go-pdf/fpdf están archivadas; signintech/gopdf se mantiene pero es de bajo nivel (sin rejilla de layout); Maroto v2 se mantiene pero está construido sobre el gofpdf archivado; unidoc es comercial. La comparación completa está en Comparativa de librerías PDF en Go 2026.
Prueba gpdf
gpdf es una librería de PDF para Go. MIT, cero dependencias, CJK nativo.
go get github.com/gpdf-dev/gpdf
⭐ Star en GitHub · Leer los docs
Lecturas siguientes
- Por qué gpdf es 10–30× más rápido que otras librerías PDF en Go — la arquitectura detrás de los números de este post
- Comparativa de librerías PDF en Go 2026 — comparación entre librerías Go nativas
- Migrar de gofpdf a gpdf — si te estás moviendo desde una librería archivada