[{"data":1,"prerenderedAt":1690},["ShallowReactive",2],{"blog-es-why-gpdf-is-faster":3},{"id":4,"title":5,"author":6,"body":9,"date":1676,"description":1677,"draft":1678,"extension":1679,"howTo":1680,"image":1680,"meta":1681,"navigation":959,"path":1682,"seo":1683,"stem":1684,"tags":1685,"updated":1680,"__hash__":1689},"blogEs/es/blog/011.why-gpdf-is-faster.md","Por qué gpdf es 10–30 veces más rápido que otras bibliotecas Go PDF",{"name":7,"url":8},"gpdf team","https://gpdf.dev",{"type":10,"value":11,"toc":1654},"minimark",[12,17,43,69,72,85,88,92,99,215,225,228,235,239,242,252,263,270,277,610,647,670,673,678,681,699,706,717,720,727,730,737,834,864,867,870,1023,1029,1039,1043,1049,1055,1061,1064,1070,1073,1077,1087,1090,1094,1097,1100,1103,1133,1140,1143,1150,1281,1284,1288,1302,1305,1309,1322,1329,1333,1336,1414,1417,1420,1424,1427,1433,1443,1457,1466,1469,1473,1476,1519,1528,1531,1543,1547,1557,1563,1576,1582,1588,1592,1595,1610,1623,1627,1650],[13,14,16],"h2",{"id":15},"tldr","TL;DR",[18,19,20,21,25,26,29,30,33,34,38,39,42],"p",{},"gpdf genera una página en ",[22,23,24],"strong",{},"13 µs",", una tabla de factura 4×10 en ",[22,27,28],{},"108 µs"," y un informe paginado de 100 páginas en ",[22,31,32],{},"683 µs",". La siguiente biblioteca Go PDF más rápida en mantenimiento — ",[35,36,37],"code",{},"jung-kurt/gofpdf"," — hace las mismas 100 páginas en ",[22,40,41],{},"11.7 ms",", unas 17 veces más lento. No es una diferencia de tuning. Son tres decisiones de diseño que se apilan:",[44,45,46,53,63],"ol",{},[47,48,49,52],"li",{},[22,50,51],{},"Layout de una sola pasada."," Sin AST intermedio entre la API Builder y el flujo de contenido PDF.",[47,54,55,58,59,62],{},[22,56,57],{},"Tipos concretos en el camino caliente."," Sin reflexión, sin ",[35,60,61],{},"interface{}",", sin despacho virtual dentro del bucle de layout.",[47,64,65,68],{},[22,66,67],{},"Un subconjuntador TrueType que resuelve el cmap una vez."," No una vez por glifo. No una vez por página. Una vez.",[18,70,71],{},"Cualquiera de los tres te da 2–3×. Apilados, te da un orden de magnitud.",[18,73,74,75,84],{},"Este artículo recorre el camino de código que produce esos números. El código de los benchmarks es público — ",[76,77,81],"a",{"href":78,"rel":79},"https://github.com/gpdf-dev/gpdf/tree/main/_benchmark",[80],"nofollow",[35,82,83],{},"_benchmark/benchmark_test.go"," — clónalo, vuelve a correrlo en tu hardware, abre un issue si los números no coinciden.",[18,86,87],{},"Aviso de sesgo por adelantado: somos el equipo de gpdf. La versión honesta de \"somos más rápidos\" es \"tomamos un conjunto distinto de compromisos\", y la pregunta interesante es qué sacrificamos para llegar aquí. Esa es la segunda mitad del artículo.",[13,89,91],{"id":90},"qué-significa-rápido-aquí","Qué significa \"rápido\" aquí",[18,93,94,95,98],{},"Antes de la arquitectura, el marcador que vamos a explicar (Apple M1, Go 1.25, sin CGO, ",[35,96,97],{},"-benchmem"," activo):",[100,101,102,127],"table",{},[103,104,105],"thead",{},[106,107,108,112,115,118,121,124],"tr",{},[109,110,111],"th",{},"Carga de trabajo",[109,113,114],{},"gpdf",[109,116,117],{},"gofpdf",[109,119,120],{},"go-pdf/fpdf",[109,122,123],{},"signintech/gopdf",[109,125,126],{},"Maroto v2",[128,129,130,152,173,193],"tbody",{},[106,131,132,136,140,143,146,149],{},[133,134,135],"td",{},"Página única Hello World",[133,137,138],{},[22,139,24],{},[133,141,142],{},"132 µs",[133,144,145],{},"135 µs",[133,147,148],{},"423 µs",[133,150,151],{},"237 µs",[106,153,154,157,161,164,167,170],{},[133,155,156],{},"Tabla de factura 4×10",[133,158,159],{},[22,160,28],{},[133,162,163],{},"241 µs",[133,165,166],{},"243 µs",[133,168,169],{},"835 µs",[133,171,172],{},"8,600 µs",[106,174,175,178,182,185,188,190],{},[133,176,177],{},"Informe paginado de 100 págs",[133,179,180],{},[22,181,32],{},[133,183,184],{},"11,700 µs",[133,186,187],{},"11,900 µs",[133,189,172],{},[133,191,192],{},"19,800 µs",[106,194,195,198,203,206,209,212],{},[133,196,197],{},"Factura CJK compleja",[133,199,200],{},[22,201,202],{},"133 µs",[133,204,205],{},"254 µs",[133,207,208],{},"n/a",[133,210,211],{},"997 µs",[133,213,214],{},"10,400 µs",[18,216,217,218,221,222,224],{},"Dos formas que se ven antes de explicarlas. El hueco ",[22,219,220],{},"se ensancha"," con el número de páginas — 10× en una hoja, 17× en 100 páginas. Y el hueco ",[22,223,220],{}," con la complejidad — 108 µs para una tabla frente a 8.6 ms para esa misma tabla a través del backend gofpdf de Maroto.",[18,226,227],{},"Ambas formas vienen de la misma raíz: el coste por elemento en gpdf es casi plano, porque el bucle de layout no asigna memoria en el camino común. Veremos por qué.",[18,229,230,231,234],{},"Aviso breve que nadie quiere leer pero lo escribimos igual: ",[22,232,233],{},"la velocidad absoluta importa menos de lo que se piensa para la mayoría de cargas de PDF",". Si tu documento más grande es un recibo de una página, cualquier biblioteca mantenida de esa tabla genera en el camino de la petición. El umbral que importa es \"puedo generar 100 de éstos de forma síncrona sin encolar\", y ahí empieza a abrirse el hueco.",[13,236,238],{"id":237},"decisión-1-sin-ast-intermedio","Decisión 1: Sin AST intermedio",[18,240,241],{},"La mayoría de bibliotecas Builder de PDF funcionan así:",[243,244,249],"pre",{"className":245,"code":247,"language":248},[246],"language-text","API Builder → árbol de documento (AST) → pasada de layout → serializador → bytes\n","text",[35,250,247],{"__ignoreMap":251},"",[18,253,254,255,258,259,262],{},"El paso del árbol de documento es el problema. Cada llamada a ",[35,256,257],{},".Text()"," asigna un nodo. Cada ",[35,260,261],{},".Row()"," asigna un contenedor. La pasada de layout recorre el árbol para calcular posiciones. Después el serializador lo recorre otra vez para emitir bytes. Tres pasadas, tres conjuntos de asignaciones, tres vueltas sobre los mismos datos por la caché de CPU.",[18,264,265,266,269],{},"gpdf no tiene el paso 2. El Builder escribe directamente a un contexto de layout que escribe directamente al flujo de contenido. ",[22,267,268],{},"Una pasada",".",[18,271,272,273,276],{},"Este es el camino de código concreto para un elemento de texto, recortado por espacio (la versión real está en ",[35,274,275],{},"template/col_builder.go","):",[243,278,282],{"className":279,"code":280,"language":281,"meta":251,"style":251},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","func (c *ColBuilder) Text(s string, opts ...TextOption) {\n    opt := c.resolveOptions(opts)\n    box := c.currentBox()\n    w := c.measureText(s, opt)\n    h := opt.FontSize.Pt() * opt.LineHeight\n    c.writer.BeginText()\n    c.writer.SetFont(opt.Font, opt.FontSize)\n    c.writer.MoveTo(box.X, box.Y-opt.FontSize.Pt())\n    c.writer.ShowString(s)\n    c.writer.EndText()\n    c.advance(w, h)\n}\n","go",[35,283,284,341,367,385,411,444,462,496,546,566,582,604],{"__ignoreMap":251},[285,286,289,293,296,300,303,307,310,314,317,320,324,327,330,333,336,338],"span",{"class":287,"line":288},"line",1,[285,290,292],{"class":291},"sMK4o","func",[285,294,295],{"class":291}," (",[285,297,299],{"class":298},"sHdIc","c ",[285,301,302],{"class":291},"*",[285,304,306],{"class":305},"sBMFI","ColBuilder",[285,308,309],{"class":291},")",[285,311,313],{"class":312},"s2Zo4"," Text",[285,315,316],{"class":291},"(",[285,318,319],{"class":298},"s",[285,321,323],{"class":322},"spNyl"," string",[285,325,326],{"class":291},",",[285,328,329],{"class":298}," opts",[285,331,332],{"class":291}," ...",[285,334,335],{"class":305},"TextOption",[285,337,309],{"class":291},[285,339,340],{"class":291}," {\n",[285,342,344,348,351,354,356,359,361,364],{"class":287,"line":343},2,[285,345,347],{"class":346},"sTEyZ","    opt ",[285,349,350],{"class":291},":=",[285,352,353],{"class":346}," c",[285,355,269],{"class":291},[285,357,358],{"class":312},"resolveOptions",[285,360,316],{"class":291},[285,362,363],{"class":346},"opts",[285,365,366],{"class":291},")\n",[285,368,370,373,375,377,379,382],{"class":287,"line":369},3,[285,371,372],{"class":346},"    box ",[285,374,350],{"class":291},[285,376,353],{"class":346},[285,378,269],{"class":291},[285,380,381],{"class":312},"currentBox",[285,383,384],{"class":291},"()\n",[285,386,388,391,393,395,397,400,402,404,406,409],{"class":287,"line":387},4,[285,389,390],{"class":346},"    w ",[285,392,350],{"class":291},[285,394,353],{"class":346},[285,396,269],{"class":291},[285,398,399],{"class":312},"measureText",[285,401,316],{"class":291},[285,403,319],{"class":346},[285,405,326],{"class":291},[285,407,408],{"class":346}," opt",[285,410,366],{"class":291},[285,412,414,417,419,421,423,426,428,431,434,437,439,441],{"class":287,"line":413},5,[285,415,416],{"class":346},"    h ",[285,418,350],{"class":291},[285,420,408],{"class":346},[285,422,269],{"class":291},[285,424,425],{"class":346},"FontSize",[285,427,269],{"class":291},[285,429,430],{"class":312},"Pt",[285,432,433],{"class":291},"()",[285,435,436],{"class":291}," *",[285,438,408],{"class":346},[285,440,269],{"class":291},[285,442,443],{"class":346},"LineHeight\n",[285,445,447,450,452,455,457,460],{"class":287,"line":446},6,[285,448,449],{"class":346},"    c",[285,451,269],{"class":291},[285,453,454],{"class":346},"writer",[285,456,269],{"class":291},[285,458,459],{"class":312},"BeginText",[285,461,384],{"class":291},[285,463,465,467,469,471,473,476,478,481,483,486,488,490,492,494],{"class":287,"line":464},7,[285,466,449],{"class":346},[285,468,269],{"class":291},[285,470,454],{"class":346},[285,472,269],{"class":291},[285,474,475],{"class":312},"SetFont",[285,477,316],{"class":291},[285,479,480],{"class":346},"opt",[285,482,269],{"class":291},[285,484,485],{"class":346},"Font",[285,487,326],{"class":291},[285,489,408],{"class":346},[285,491,269],{"class":291},[285,493,425],{"class":346},[285,495,366],{"class":291},[285,497,499,501,503,505,507,510,512,515,517,520,522,525,527,530,533,535,537,539,541,543],{"class":287,"line":498},8,[285,500,449],{"class":346},[285,502,269],{"class":291},[285,504,454],{"class":346},[285,506,269],{"class":291},[285,508,509],{"class":312},"MoveTo",[285,511,316],{"class":291},[285,513,514],{"class":346},"box",[285,516,269],{"class":291},[285,518,519],{"class":346},"X",[285,521,326],{"class":291},[285,523,524],{"class":346}," box",[285,526,269],{"class":291},[285,528,529],{"class":346},"Y",[285,531,532],{"class":291},"-",[285,534,480],{"class":346},[285,536,269],{"class":291},[285,538,425],{"class":346},[285,540,269],{"class":291},[285,542,430],{"class":312},[285,544,545],{"class":291},"())\n",[285,547,549,551,553,555,557,560,562,564],{"class":287,"line":548},9,[285,550,449],{"class":346},[285,552,269],{"class":291},[285,554,454],{"class":346},[285,556,269],{"class":291},[285,558,559],{"class":312},"ShowString",[285,561,316],{"class":291},[285,563,319],{"class":346},[285,565,366],{"class":291},[285,567,569,571,573,575,577,580],{"class":287,"line":568},10,[285,570,449],{"class":346},[285,572,269],{"class":291},[285,574,454],{"class":346},[285,576,269],{"class":291},[285,578,579],{"class":312},"EndText",[285,581,384],{"class":291},[285,583,585,587,589,592,594,597,599,602],{"class":287,"line":584},11,[285,586,449],{"class":346},[285,588,269],{"class":291},[285,590,591],{"class":312},"advance",[285,593,316],{"class":291},[285,595,596],{"class":346},"w",[285,598,326],{"class":291},[285,600,601],{"class":346}," h",[285,603,366],{"class":291},[285,605,607],{"class":287,"line":606},12,[285,608,609],{"class":291},"}\n",[18,611,612,613,616,617,620,621,624,625,627,628,627,630,632,633,636,637,636,640,636,643,646],{},"Ningún nodo se mete en un árbol. Ninguna posición se difiere. El writer es un ",[35,614,615],{},"*pdf.Writer"," que mantiene un ",[35,618,619],{},"io.Writer"," (normalmente un ",[35,622,623],{},"bytes.Buffer","), y ",[35,626,459],{}," / ",[35,629,509],{},[35,631,559],{}," escriben los operadores PDF (",[35,634,635],{},"BT",", ",[35,638,639],{},"Td",[35,641,642],{},"Tj",[35,644,645],{},"ET",") inmediatamente al buffer.",[18,648,649,650,653,654,657,658,661,662,665,666,669],{},"Compara cómo hace gofpdf la misma operación lógica. gofpdf mantiene un objeto ",[35,651,652],{},"page"," con un slice de operaciones. Cada llamada ",[35,655,656],{},"SetXY"," + ",[35,659,660],{},"Cell"," añade a ese slice. ",[35,663,664],{},"Output"," (o ",[35,667,668],{},"OutputFileAndClose",") recorre el slice al final y emite los bytes. Son dos asignaciones por celda — una por la estructura de operación, otra por la copia de string — y una pasada extra sobre los datos.",[18,671,672],{},"Para un informe de 100 páginas con ~40 líneas por página, son 4,000 asignaciones extra que gpdf no hace.",[674,675,677],"h3",{"id":676},"dónde-duele-la-pasada-única","Dónde duele la pasada única",[18,679,680],{},"La pregunta obvia: ¿cómo se hace lo que necesita conocer el layout final de página antes de empezar a emitir bytes? Cabeceras con números de página. Tablas que cruzan páginas. Pies de página anclados al final de la última línea del cuerpo.",[18,682,683,684,687,688,691,692,636,695,698],{},"Dos respuestas. Primera: bufferizamos la ",[22,685,686],{},"página"," actual, no el documento. Una página es una unidad acotada — decenas de KB, no megabytes. Cuando se ejecuta el siguiente ",[35,689,690],{},"AddPage()",", el flujo de contenido de la página actual se cierra (",[35,693,694],{},"Length",[35,696,697],{},"Filter",", offsets), se escribe su entrada xref y el buffer de página se resetea. El pico de memoria se mantiene en O(una página).",[18,700,701,702,705],{},"Segunda: para elementos realmente globales (\"Página 3 de 27\"), diferimos ",[22,703,704],{},"ese rango específico"," a una pasada de fix-up. El resto del contenido ya está en el flujo. El fix-up recorre una lista corta de marcadores deferred-reference y parchea. Es el único sitio del código base donde pagamos algo parecido a un coste de AST, y solo lo pagamos para el contenido que lo necesita.",[18,707,708,709,712,713,716],{},"El intercambio: no puedes hacer post-procesado arbitrario sobre un árbol de nodos, porque no hay árbol. No puedes escribir un plugin que reordene \"todos los nodos ",[35,710,711],{},"Text"," con ",[35,714,715],{},"bold: true","\". Si necesitas esa forma de API, Maroto v2 lo hace; gpdf no.",[18,718,719],{},"Creemos que es el intercambio correcto para los casos de uso que gpdf apunta. La mayoría de PDFs se producen de izquierda a derecha, de arriba a abajo, con un layout conocido en tiempo de construcción. El coste de mantener un AST para la minoría que lo necesita lo pagaba la mayoría en cada página. Invertimos esa relación.",[13,721,723,724,726],{"id":722},"decisión-2-sin-reflexión-sin-interface-en-el-camino-caliente","Decisión 2: Sin reflexión, sin ",[35,725,61],{}," en el camino caliente",[18,728,729],{},"Escribir sobre esto es menos interesante que perfilarlo. Pero de aquí viene la otra mitad de la velocidad.",[18,731,732,733,736],{},"Mira la firma de ",[35,734,735],{},"CellFormat"," de gofpdf:",[243,738,740],{"className":279,"code":739,"language":281,"meta":251,"style":251},"func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string,\n    ln int, alignStr string, fill bool, link int, linkStr string) { ... }\n",[35,741,742,787],{"__ignoreMap":251},[285,743,744,746,748,751,753,756,758,761,763,765,767,769,772,774,777,779,782,784],{"class":287,"line":288},[285,745,292],{"class":291},[285,747,295],{"class":291},[285,749,750],{"class":298},"f ",[285,752,302],{"class":291},[285,754,755],{"class":305},"Fpdf",[285,757,309],{"class":291},[285,759,760],{"class":312}," CellFormat",[285,762,316],{"class":291},[285,764,596],{"class":298},[285,766,326],{"class":291},[285,768,601],{"class":298},[285,770,771],{"class":322}," float64",[285,773,326],{"class":291},[285,775,776],{"class":298}," txtStr",[285,778,326],{"class":291},[285,780,781],{"class":298}," borderStr",[285,783,323],{"class":322},[285,785,786],{"class":291},",\n",[285,788,789,792,795,797,800,802,804,807,810,812,815,817,819,822,824,826,829,831],{"class":287,"line":343},[285,790,791],{"class":298},"    ln",[285,793,794],{"class":322}," int",[285,796,326],{"class":291},[285,798,799],{"class":298}," alignStr",[285,801,323],{"class":322},[285,803,326],{"class":291},[285,805,806],{"class":298}," fill",[285,808,809],{"class":322}," bool",[285,811,326],{"class":291},[285,813,814],{"class":298}," link",[285,816,794],{"class":322},[285,818,326],{"class":291},[285,820,821],{"class":298}," linkStr",[285,823,323],{"class":322},[285,825,309],{"class":291},[285,827,828],{"class":291}," {",[285,830,332],{"class":291},[285,832,833],{"class":291}," }\n",[18,835,836,837,840,841,844,845,848,849,852,853,856,857,859,860,863],{},"Vale. Ahora mira el árbol de componentes de Maroto. Un ",[35,838,839],{},"Row"," tiene ",[35,842,843],{},"[]Component",". Un ",[35,846,847],{},"Component"," es una interfaz. Cada operación de layout es un despacho virtual: ",[35,850,851],{},"component.Render(ctx)",". Para un único ",[35,854,855],{},"Col"," con un ",[35,858,711],{}," y un ",[35,861,862],{},"Spacer",", son tres despachos. En un informe de 100 páginas con ~30 filas por página y ~3 componentes por fila, son ~9,000 despachos.",[18,865,866],{},"Individualmente, un despacho de interfaz en Go son ~2–3 ns. No es un crimen. Pero el despacho también obliga al compilador a mantener el valor boxeado en el heap — no puedes stack-allocate a través de una interfaz sin una pasada de devirtualización que el compilador de Go no siempre hace. Así que el coste no es solo el despacho; es la asignación que lo alimenta.",[18,868,869],{},"El motor de layout de gpdf usa structs concretos:",[243,871,873],{"className":279,"code":872,"language":281,"meta":251,"style":251},"type RowBuilder struct {\n    doc    *Document\n    parent *pageState\n    spans  [12]int\n    cols   [12]ColBuilder  // valor, no puntero, no interfaz\n    n      uint8\n}\n\ntype ColBuilder struct {\n    row    *RowBuilder\n    span   int\n    cursor document.Point\n    writer *pdf.Writer\n}\n",[35,874,875,888,898,908,926,943,951,955,961,972,982,989,1002,1018],{"__ignoreMap":251},[285,876,877,880,883,886],{"class":287,"line":288},[285,878,879],{"class":291},"type",[285,881,882],{"class":305}," RowBuilder",[285,884,885],{"class":291}," struct",[285,887,340],{"class":291},[285,889,890,893,895],{"class":287,"line":343},[285,891,892],{"class":346},"    doc    ",[285,894,302],{"class":291},[285,896,897],{"class":305},"Document\n",[285,899,900,903,905],{"class":287,"line":369},[285,901,902],{"class":346},"    parent ",[285,904,302],{"class":291},[285,906,907],{"class":305},"pageState\n",[285,909,910,913,916,920,923],{"class":287,"line":387},[285,911,912],{"class":346},"    spans  ",[285,914,915],{"class":291},"[",[285,917,919],{"class":918},"sbssI","12",[285,921,922],{"class":291},"]",[285,924,925],{"class":322},"int\n",[285,927,928,931,933,935,937,939],{"class":287,"line":413},[285,929,930],{"class":346},"    cols   ",[285,932,915],{"class":291},[285,934,919],{"class":918},[285,936,922],{"class":291},[285,938,306],{"class":305},[285,940,942],{"class":941},"sHwdD","  // valor, no puntero, no interfaz\n",[285,944,945,948],{"class":287,"line":446},[285,946,947],{"class":346},"    n      ",[285,949,950],{"class":322},"uint8\n",[285,952,953],{"class":287,"line":464},[285,954,609],{"class":291},[285,956,957],{"class":287,"line":498},[285,958,960],{"emptyLinePlaceholder":959},true,"\n",[285,962,963,965,968,970],{"class":287,"line":548},[285,964,879],{"class":291},[285,966,967],{"class":305}," ColBuilder",[285,969,885],{"class":291},[285,971,340],{"class":291},[285,973,974,977,979],{"class":287,"line":568},[285,975,976],{"class":346},"    row    ",[285,978,302],{"class":291},[285,980,981],{"class":305},"RowBuilder\n",[285,983,984,987],{"class":287,"line":584},[285,985,986],{"class":346},"    span   ",[285,988,925],{"class":322},[285,990,991,994,997,999],{"class":287,"line":606},[285,992,993],{"class":346},"    cursor ",[285,995,996],{"class":305},"document",[285,998,269],{"class":291},[285,1000,1001],{"class":305},"Point\n",[285,1003,1005,1008,1010,1013,1015],{"class":287,"line":1004},13,[285,1006,1007],{"class":346},"    writer ",[285,1009,302],{"class":291},[285,1011,1012],{"class":305},"pdf",[285,1014,269],{"class":291},[285,1016,1017],{"class":305},"Writer\n",[285,1019,1021],{"class":287,"line":1020},14,[285,1022,609],{"class":291},[18,1024,1025,1028],{},[35,1026,1027],{},"cols"," es un array de valores, dimensionado al número máximo de columnas (12, del sistema de grid). Sin asignación en heap. Sin despacho de interfaz cuando la fila itera sus columnas. El Builder mantiene un puntero al writer, no al revés — el writer no conoce el árbol del Builder.",[18,1030,1031,1032,1035,1036,1038],{},"El patrón de callback (",[35,1033,1034],{},"r.Col(4, func(c *ColBuilder) { ... })",") no es accidente. Todas las otras formas que prototipamos — una API que devuelve structs encadenables, un árbol de interfaces Component boxeadas — eran más lentas. El closure tiene cero asignaciones porque el ",[35,1037,306],{}," es un valor que el llamante mantiene por puntero vía el parámetro; el propio closure se escape-analiza a la pila en el caso común.",[674,1040,1042],{"id":1041},"cómo-sabemos-que-funcionó","Cómo sabemos que funcionó",[18,1044,1045,1048],{},[35,1046,1047],{},"go test -run=XXX -bench=BenchmarkSinglePage -memprofile=mem.out"," en gpdf da un número del que estamos orgullosos:",[243,1050,1053],{"className":1051,"code":1052,"language":248},[246],"BenchmarkSinglePage-8   91270   13120 ns/op   8321 B/op   52 allocs/op\n",[35,1054,1052],{"__ignoreMap":251},[18,1056,1057,1058,1060],{},"Cincuenta y dos asignaciones para una página PDF entera. Casi todas son el buffer inicial de página, la búsqueda de métricas de fuente (una vez por fuente, no una por glifo) y el crecimiento final del ",[35,1059,623],{},". El bucle de layout asigna cero — mira el perfil.",[18,1062,1063],{},"gofpdf en la misma página:",[243,1065,1068],{"className":1066,"code":1067,"language":248},[246],"BenchmarkGofpdfSinglePage-8   7500   132400 ns/op   71200 B/op   430 allocs/op\n",[35,1069,1067],{"__ignoreMap":251},[18,1071,1072],{},"430 asignaciones. La mayoría son el slice de operaciones y las copias de string que lo alimentan. Mueve ese factor ~8 en asignaciones a través del GC, y el hueco de runtime de ~10× sale mecánicamente.",[674,1074,1076],{"id":1075},"qué-cedimos","Qué cedimos",[18,1078,1079,1080,1082,1083,1086],{},"Cero ergonomía en el camino caliente significa menos puntos de extensión. Si quieres escribir un tipo de elemento personalizado que se enchufe al layout de gpdf — el equivalente a implementar ",[35,1081,847],{}," en Maroto — no puedes. No hay interfaz que satisfacer. Lo que ofrecemos en su lugar es ",[35,1084,1085],{},"template.WithWriterSetup()",", que da un hook al writer PDF para cosas como anotaciones personalizadas, metadatos PDF/A o cifrado. Para extensión de layout, lo escribes como un helper que llama a los mismos métodos Builder que un usuario llamaría.",[18,1088,1089],{},"Menos puntos de extensión es un coste real. Hemos decidido que vale la pena. Si la forma del proyecto cambia en una dirección donde no lo vale, lo revisaremos.",[13,1091,1093],{"id":1092},"decisión-3-subconjuntado-truetype-sin-re-recorridos","Decisión 3: Subconjuntado TrueType sin re-recorridos",[18,1095,1096],{},"Aquí es donde el benchmark CJK (133 µs frente a 254 µs de gofpdf) se lleva la mayor parte del hueco.",[18,1098,1099],{},"Resumen rápido de lo que hace el subconjuntado TrueType. Cuando embebes una fuente japonesa en un PDF, no quieres embeber sus 20,000+ glifos — son 15 MB de datos de fuente en un documento de 100 KB. Quieres embeber solo los glifos que tu documento realmente usa, empaquetados como un TTF subconjunto válido que un lector PDF pueda decodificar.",[18,1101,1102],{},"Para hacerlo:",[44,1104,1105,1124,1127,1130],{},[47,1106,1107,1108,1111,1112,1115,1116,1119,1120,1123],{},"Parsear las tablas TTF completas: ",[35,1109,1110],{},"cmap"," (mapeo carácter→glifo), ",[35,1113,1114],{},"glyf"," (contornos), ",[35,1117,1118],{},"loca"," (offsets a glyf), ",[35,1121,1122],{},"hmtx"," (métricas horizontales), etc.",[47,1125,1126],{},"Para cada carácter que usa el documento, buscar su ID de glifo vía el cmap.",[47,1128,1129],{},"Recolectar transitivamente los glifos que los glifos compuestos referencian.",[47,1131,1132],{},"Emitir un TTF nuevo con solo esos glifos, renumerados.",[18,1134,1135,1136,1139],{},"El paso 2 — la búsqueda en cmap — es el camino caliente. La implementación de gofpdf ",[22,1137,1138],{},"recorre la tabla cmap desde el principio en cada búsqueda de glifo",". Para una página solo Latin va bien; el cmap es pequeño y la caché se porta. Para una página CJK con 150 glifos únicos son 150 recorridos completos de la tabla.",[18,1141,1142],{},"El formato 12 del cmap (usado por la mayoría de fuentes CJK modernas) es un array ordenado de triples (start, end, startGlyphID). Un recorrido es O(n) en el número de rangos, ~200–500 para NotoSansJP. 150 búsquedas de glifo × 400 rangos × comparación por rango = mucho más trabajo del necesario.",[18,1144,1145,1146,1149],{},"gpdf resuelve el cmap entero a un ",[35,1147,1148],{},"map[rune]uint16"," en la primera carga de la fuente. A partir de ahí, cada búsqueda es O(1). Para NotoSansJP, el coste único es ~150 µs; después, 10 ns por carácter.",[243,1151,1153],{"className":279,"code":1152,"language":281,"meta":251,"style":251},"// Simplificado de pdf/font/ttf.go\ntype Font struct {\n    runeToGID map[rune]uint16  // resuelto una vez al cargar\n    glyphs    []glyph          // indexado por GID\n    metrics   []glyphMetric\n}\n\nfunc (f *Font) GlyphFor(r rune) uint16 {\n    return f.runeToGID[r]  // O(1), amable con caché, sin recorrido\n}\n",[35,1154,1155,1160,1171,1190,1204,1214,1218,1222,1254,1277],{"__ignoreMap":251},[285,1156,1157],{"class":287,"line":288},[285,1158,1159],{"class":941},"// Simplificado de pdf/font/ttf.go\n",[285,1161,1162,1164,1167,1169],{"class":287,"line":343},[285,1163,879],{"class":291},[285,1165,1166],{"class":305}," Font",[285,1168,885],{"class":291},[285,1170,340],{"class":291},[285,1172,1173,1176,1179,1182,1184,1187],{"class":287,"line":369},[285,1174,1175],{"class":346},"    runeToGID ",[285,1177,1178],{"class":291},"map[",[285,1180,1181],{"class":322},"rune",[285,1183,922],{"class":291},[285,1185,1186],{"class":322},"uint16",[285,1188,1189],{"class":941},"  // resuelto una vez al cargar\n",[285,1191,1192,1195,1198,1201],{"class":287,"line":387},[285,1193,1194],{"class":346},"    glyphs    ",[285,1196,1197],{"class":291},"[]",[285,1199,1200],{"class":305},"glyph",[285,1202,1203],{"class":941},"          // indexado por GID\n",[285,1205,1206,1209,1211],{"class":287,"line":413},[285,1207,1208],{"class":346},"    metrics   ",[285,1210,1197],{"class":291},[285,1212,1213],{"class":305},"glyphMetric\n",[285,1215,1216],{"class":287,"line":446},[285,1217,609],{"class":291},[285,1219,1220],{"class":287,"line":464},[285,1221,960],{"emptyLinePlaceholder":959},[285,1223,1224,1226,1228,1230,1232,1234,1236,1239,1241,1244,1247,1249,1252],{"class":287,"line":498},[285,1225,292],{"class":291},[285,1227,295],{"class":291},[285,1229,750],{"class":298},[285,1231,302],{"class":291},[285,1233,485],{"class":305},[285,1235,309],{"class":291},[285,1237,1238],{"class":312}," GlyphFor",[285,1240,316],{"class":291},[285,1242,1243],{"class":298},"r",[285,1245,1246],{"class":322}," rune",[285,1248,309],{"class":291},[285,1250,1251],{"class":322}," uint16",[285,1253,340],{"class":291},[285,1255,1256,1260,1263,1265,1268,1270,1272,1274],{"class":287,"line":548},[285,1257,1259],{"class":1258},"s7zQu","    return",[285,1261,1262],{"class":346}," f",[285,1264,269],{"class":291},[285,1266,1267],{"class":346},"runeToGID",[285,1269,915],{"class":291},[285,1271,1243],{"class":346},[285,1273,922],{"class":291},[285,1275,1276],{"class":941},"  // O(1), amable con caché, sin recorrido\n",[285,1278,1279],{"class":287,"line":568},[285,1280,609],{"class":291},[18,1282,1283],{},"Un mapa indexado por rune, poblado por una pasada lineal de la tabla cmap. Para un documento que usa la misma fuente en varias páginas (todas), esto mueve la búsqueda de glifos de \"cuasi-cuadrática en páginas × glifos\" a \"lineal en glifos totales más una constante fija\".",[674,1285,1287],{"id":1286},"por-qué-el-format-12-es-el-detalle-que-importa","Por qué el \"format 12\" es el detalle que importa",[18,1289,1290,1291,1294,1295,1297,1298,1301],{},"La mayoría de las bibliotecas Go PDF antiguas se escribieron cuando el texto Latin era lo único que importaba, e implementaron el cmap format 4 — un rango segmentado para el Basic Multilingual Plane (U+0000–U+FFFF). El japonés fuera del BMP (menos común, pero algunas variantes Kanji) necesita format 12. El ",[35,1292,1293],{},"AddUTF8Font"," de ",[35,1296,120],{}," ",[22,1299,1300],{},"hace panic"," en NotoSansJP-Regular.ttf porque su parser de format 12 no se terminó.",[18,1303,1304],{},"No es una crítica. Es un artefacto: gofpdf fue una gran biblioteca para lo que las webs centradas en Latin necesitaban en 2015, y el fork heredó su alcance. El mundo se movió; el CJK pasó de \"el problema de otro\" a \"el problema de la mayoría de los ecosistemas Go de Japón y China\". gpdf implementó la especificación cmap completa porque la alternativa era una factura que muestra cajas de tofu para 品目 — un bug reportado en la primera semana de release pública.",[674,1306,1308],{"id":1307},"caché-que-escala-con-número-de-fuentes-no-con-tamaño-de-documento","Caché que escala con número de fuentes, no con tamaño de documento",[18,1310,1311,1312,1315,1316,1318,1319,269],{},"La caché de fuentes es por ",[35,1313,1314],{},"Document",", no global. Si generas 10,000 PDFs con la misma fuente, pagas el coste de resolución de 150 µs 10,000 veces — a menos que compartas una instancia ",[35,1317,485],{}," entre documentos, cosa que la API permite vía ",[35,1320,1321],{},"gpdf.WithSharedFont(preloadedFont)",[18,1323,1324,1325,1328],{},"Para generación en lotes de alto volumen (la SaaS ",[35,1326,1327],{},"gpdf-api"," funciona así), el patrón de fuente compartida es lo que hace predecible la latencia P95. Lo publicamos en los docs; la mayoría de usuarios OSS no lo necesita.",[13,1330,1332],{"id":1331},"el-efecto-combinado","El efecto combinado",[18,1334,1335],{},"Pongamos las tres decisiones lado a lado en el benchmark de 100 páginas (683 µs para gpdf, 11.7 ms para gofpdf):",[100,1337,1338,1351],{},[103,1339,1340],{},[106,1341,1342,1345,1348],{},[109,1343,1344],{},"Origen del tiempo",[109,1346,1347],{},"gofpdf (por página, aprox)",[109,1349,1350],{},"gpdf (por página, aprox)",[128,1352,1353,1364,1375,1386,1397],{},[106,1354,1355,1358,1361],{},[133,1356,1357],{},"Construcción del slice ops",[133,1359,1360],{},"~60 µs",[133,1362,1363],{},"0 (stream directo)",[106,1365,1366,1369,1372],{},[133,1367,1368],{},"Serialización de ops",[133,1370,1371],{},"~35 µs",[133,1373,1374],{},"0 (ya escrito)",[106,1376,1377,1380,1383],{},[133,1378,1379],{},"Búsquedas de glifo (40 chars)",[133,1381,1382],{},"~6 µs",[133,1384,1385],{},"~0.4 µs",[106,1387,1388,1391,1394],{},[133,1389,1390],{},"Asignación / presión GC",[133,1392,1393],{},"~20 µs",[133,1395,1396],{},"~2 µs",[106,1398,1399,1404,1409],{},[133,1400,1401],{},[22,1402,1403],{},"Total",[133,1405,1406],{},[22,1407,1408],{},"~120 µs",[133,1410,1411],{},[22,1412,1413],{},"~7 µs",[18,1415,1416],{},"Los números son estimaciones de profiling; el desglose real depende del contenido. Pero la forma es correcta. Ninguno de los tres diseños gana 10× solo. Se suman.",[18,1418,1419],{},"Corolario: si copias solo un diseño a una biblioteca existente, ganas 2–3×. Si quieres el 10×, necesitas los tres, y no puedes retrofit el primero en una biblioteca basada en AST sin reescribirla.",[13,1421,1423],{"id":1422},"lo-que-cedimos-la-sección-honesta","Lo que cedimos (la sección honesta)",[18,1425,1426],{},"Hemos estado bailando alrededor. La lista completa:",[18,1428,1429,1432],{},[22,1430,1431],{},"Post-procesado basado en AST."," Sin arquitectura de plugins. Sin \"recorre el árbol de nodos y aplica esta transformación\". Si quieres editar estilos de texto globalmente antes de renderizar, lo haces antes de llamar al Builder, no después.",[18,1434,1435,1438,1439,1442],{},[22,1436,1437],{},"Introspección."," No hay ",[35,1440,1441],{},"doc.Components()"," que devuelva todo lo que pusiste. El documento es un flujo de operadores para cuando cualquier método significativo pueda correr. Para la mayoría de usuarios esto nunca aparece; para la minoría que escribe herramientas de manipulación de documentos, sí.",[18,1444,1445,1448,1449,1452,1453,1456],{},[22,1446,1447],{},"Serialización por reflexión."," No tenemos una API estilo ",[35,1450,1451],{},"json.Unmarshal"," que convierta structs arbitrarios en PDF. El punto de entrada JSON Schema (",[35,1454,1455],{},"template.FromJSON",") es explícito sobre sus formas soportadas, a propósito. Si quieres apuntar una biblioteca a un struct Go genérico y obtener un PDF, eso es territorio de unidoc.",[18,1458,1459,1462,1463,1465],{},[22,1460,1461],{},"La extensibilidad de una interfaz."," No puedes implementar ",[35,1464,847],{}," y registrar un elemento personalizado. Puedes escribir una función helper que envuelve las llamadas al Builder, y en la práctica eso cubre el 95% de lo que pide la gente, pero es un modelo distinto.",[18,1467,1468],{},"Son deliberados. Cada uno individualmente mataría la velocidad. Elegimos el grupo de usuarios cuyo trabajo se beneficia de \"rápido y opinionado\" sobre el grupo que necesita \"flexible y rico en plugins\". Si estás en el segundo grupo, Maroto v2 o unidoc probablemente encajan mejor.",[13,1470,1472],{"id":1471},"puedo-volver-a-correr-el-benchmark","¿Puedo volver a correr el benchmark?",[18,1474,1475],{},"Sí. Ese es el propósito de publicar el código.",[243,1477,1481],{"className":1478,"code":1479,"language":1480,"meta":251,"style":251},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone https://github.com/gpdf-dev/gpdf\ncd gpdf/_benchmark\ngo test -bench=. -benchmem -benchtime=5s\n","bash",[35,1482,1483,1495,1503],{"__ignoreMap":251},[285,1484,1485,1488,1492],{"class":287,"line":288},[285,1486,1487],{"class":305},"git",[285,1489,1491],{"class":1490},"sfazB"," clone",[285,1493,1494],{"class":1490}," https://github.com/gpdf-dev/gpdf\n",[285,1496,1497,1500],{"class":287,"line":343},[285,1498,1499],{"class":312},"cd",[285,1501,1502],{"class":1490}," gpdf/_benchmark\n",[285,1504,1505,1507,1510,1513,1516],{"class":287,"line":369},[285,1506,281],{"class":305},[285,1508,1509],{"class":1490}," test",[285,1511,1512],{"class":1490}," -bench=.",[285,1514,1515],{"class":1490}," -benchmem",[285,1517,1518],{"class":1490}," -benchtime=5s\n",[18,1520,1521,1522,1527],{},"El README de ese directorio documenta las cuatro cargas y qué miden. Si tus números difieren materialmente (>20%) en la misma arquitectura de CPU y versión de Go, ",[76,1523,1526],{"href":1524,"rel":1525},"https://github.com/gpdf-dev/gpdf/issues",[80],"abre un issue"," — el drift es real y queremos saberlo.",[18,1529,1530],{},"Dos matices:",[1532,1533,1534,1540],"ul",{},[47,1535,1536,1537,1539],{},"El benchmark corre con ",[35,1538,97],{},". Si lo desactivas, los números mejoran ~5% en general, cosa que no contamos en afirmaciones públicas porque no es cómo nadie corre código real.",[47,1541,1542],{},"CGO está off. Algunos lectores han preguntado si un backend FreeType enlazado con CGO sería más rápido para operaciones de fuente; lo probamos, y el coste de marshaling a través de la frontera FFI dominó cualquier ganancia. El subconjuntador en Go puro gana para los patrones de acceso que tiene un generador PDF.",[13,1544,1546],{"id":1545},"faq","FAQ",[18,1548,1549,1552,1553,269],{},[22,1550,1551],{},"¿Por qué comparar con gofpdf si está archivado?","\nPorque sigue siendo el primer resultado de GitHub para \"go pdf\", y la mayoría de los equipos que llegan a gpdf están migrando desde allí. El benchmark necesita contestar \"¿vale la pena la migración?\" para esa audiencia. Versión corta: sí, y hemos escrito una ",[76,1554,1556],{"href":1555},"/es/blog/gofpdf-migration","guía de migración",[18,1558,1559,1562],{},[22,1560,1561],{},"¿Ser 10× más rápido es realmente significativo para generar PDFs?","\nDepende de la carga. Para un documento por petición de usuario, no mucho — ambas bibliotecas pasan el umbral de \"generar en la petición\". Para operaciones en lote (extractos nocturnos, facturas en masa, generación de informes desde queries a DB), el hueco se traduce directamente en menos máquinas. Oímos \"10× menos workers\" del primer equipo que migró su pipeline de lotes; no auditamos sus cuentas pero encaja con el benchmark.",[18,1564,1565,1568,1569,1572,1573,1575],{},[22,1566,1567],{},"¿Cuál es el truco del número CJK?","\nTodavía tienes que enviar el archivo de fuente. gpdf lo subconjunta por ti, pero un NotoSansJP TTF de 3 MB son 3 MB que o embebes en tu binario Go o haces ",[35,1570,1571],{},"os.ReadFile"," al arranque. Para imágenes distroless esto importa. La SaaS ",[35,1574,1327],{}," lo soluciona enviando las fuentes comunes en la imagen; los usuarios OSS lo manejan ellos.",[18,1577,1578,1581],{},[22,1579,1580],{},"¿gpdf se volverá lento según se añadan features?","\nEs la pregunta que más nos importa. Respuesta: hacemos benchmark de cada release frente a la anterior, y una regresión mayor al 5% en cualquiera de las cuatro cargas bloquea la release. Los benchmarks viven en el mismo repo que la biblioteca exactamente por esto.",[18,1583,1584,1587],{},[22,1585,1586],{},"¿De dónde viene el nombre?","\ngpdf = Go + PDF. No es ingenioso. Intencionalmente.",[13,1589,1591],{"id":1590},"probar-gpdf","Probar gpdf",[18,1593,1594],{},"gpdf es una biblioteca Go para generar PDFs. MIT, cero dependencias, CJK nativo.",[243,1596,1598],{"className":1478,"code":1597,"language":1480,"meta":251,"style":251},"go get github.com/gpdf-dev/gpdf\n",[35,1599,1600],{"__ignoreMap":251},[285,1601,1602,1604,1607],{"class":287,"line":288},[285,1603,281],{"class":305},[285,1605,1606],{"class":1490}," get",[285,1608,1609],{"class":1490}," github.com/gpdf-dev/gpdf\n",[18,1611,1612,1617,1618],{},[76,1613,1616],{"href":1614,"rel":1615},"https://github.com/gpdf-dev/gpdf",[80],"⭐ Star on GitHub"," · ",[76,1619,1622],{"href":1620,"rel":1621},"https://gpdf.dev/es/docs/quickstart",[80],"Leer los docs",[13,1624,1626],{"id":1625},"lecturas-siguientes","Lecturas siguientes",[1532,1628,1629,1636,1642],{},[47,1630,1631,1635],{},[76,1632,1634],{"href":1633},"/es/blog/go-pdf-library-showdown-2026","Comparativa de bibliotecas Go PDF 2026"," — comparación completa con licencias y dependencias.",[47,1637,1638,1641],{},[76,1639,1640],{"href":1555},"gofpdf está archivado. Cómo migrar a gpdf."," — cinco pares de API antes/después, todos ejecutables.",[47,1643,1644,1645,269],{},"El código del benchmark: ",[76,1646,1648],{"href":78,"rel":1647},[80],[35,1649,83],{},[1651,1652,1653],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":251,"searchDepth":343,"depth":343,"links":1655},[1656,1657,1658,1661,1666,1670,1671,1672,1673,1674,1675],{"id":15,"depth":343,"text":16},{"id":90,"depth":343,"text":91},{"id":237,"depth":343,"text":238,"children":1659},[1660],{"id":676,"depth":369,"text":677},{"id":722,"depth":343,"text":1662,"children":1663},"Decisión 2: Sin reflexión, sin interface{} en el camino caliente",[1664,1665],{"id":1041,"depth":369,"text":1042},{"id":1075,"depth":369,"text":1076},{"id":1092,"depth":343,"text":1093,"children":1667},[1668,1669],{"id":1286,"depth":369,"text":1287},{"id":1307,"depth":369,"text":1308},{"id":1331,"depth":343,"text":1332},{"id":1422,"depth":343,"text":1423},{"id":1471,"depth":343,"text":1472},{"id":1545,"depth":343,"text":1546},{"id":1590,"depth":343,"text":1591},{"id":1625,"depth":343,"text":1626},"2026-04-19","gpdf genera una página en 13 µs y un informe de 100 páginas en 683 µs. No es un truco de tuning: son tres decisiones arquitectónicas que se suman. Este artículo recorre el código.",false,"md",null,{},"/es/blog/why-gpdf-is-faster",{"title":5,"description":1677},"es/blog/011.why-gpdf-is-faster",[1686,1687,1688],"benchmark","internals","comparison","2c_TWpL4LXf3FhCx183dQFTG7lPVswdns-iOGBcMZ0A",1776537648044]