[{"data":1,"prerenderedAt":1007},["Reactive",2],{"/guides/react-to-pdf":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"dateModified":10,"author":11,"faq":12,"body":22,"_type":1002,"_id":1003,"_source":1004,"_file":1005,"_extension":1006},"/guides/react-to-pdf","guides",false,"","React to PDF: 4 ways, one invoice, measured","React to PDF four ways, one 45-row invoice: react-to-pdf, @react-pdf/renderer, Puppeteer and an API. File sizes, selectable text and page breaks.","2026-11-10","Cyril Nicodème",[13,16,19],{"question":14,"answer":15},"Is react-pdf the same package as @react-pdf/renderer?","No. @react-pdf/renderer creates PDFs from its own components (Document, Page, View, Text). The package named react-pdf displays existing PDFs inside a React app. Install the first to generate a PDF, the second to show one.",{"question":17,"answer":18},"What about react-to-print?","react-to-print prints a React component in the browser. It opens the print dialog, so a person picks Save as PDF themselves. That's fine for a print button, but your code never gets the PDF file, so you can't email it or store it.",{"question":20,"answer":21},"Can I generate a PDF in the browser without taking a screenshot?","Yes, with @react-pdf/renderer, which runs in the browser as well as in Node. Its PDFDownloadLink and BlobProvider components build a real PDF with selectable text on the user's device.",{"type":23,"children":24,"toc":987},"root",[25,33,40,54,56,212,363,369,385,489,501,506,511,517,593,605,614,626,631,637,657,666,694,739,752,758,779,790,811,816,829,835,848,911,917,923,953,958,962,967],{"type":26,"tag":27,"props":28,"children":29},"element","p",{},[30],{"type":31,"value":32},"text","Render the React component you already have in a real browser, on the server. In our test of one 45-row invoice, that gave a 64 KB to 103 KB PDF with every word selectable, the CSS grid intact and no row cut in half. react-to-pdf gave a 1.74 MB PDF with 2 characters of text. @react-pdf/renderer needed a second copy of the component and split a row until we told it not to. So the choice is: run that browser yourself with Puppeteer, or send us the HTML and we run it.",{"type":26,"tag":34,"props":35,"children":37},"h2",{"id":36},"how-do-the-4-react-to-pdf-methods-compare",[38],{"type":31,"value":39},"How do the 4 React to PDF methods compare?",{"type":26,"tag":27,"props":41,"children":42},{},[43,45,52],{"type":31,"value":44},"We built one invoice the way a web developer would: a flex header, a CSS grid for the bill-to and ship-to blocks, a ",{"type":26,"tag":46,"props":47,"children":49},"code",{"className":48},[],[50],{"type":31,"value":51},"\u003Ctable>",{"type":31,"value":53}," of 45 line items that runs onto a second A4 page, and a totals block. Then we turned it into a PDF four ways and measured each file:",{"type":31,"value":55},"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",{"type":26,"tag":57,"props":58,"children":59},"table",{},[60,89],{"type":26,"tag":61,"props":62,"children":63},"thead",{},[64],{"type":26,"tag":65,"props":66,"children":67},"tr",{},[68,74,79,84],{"type":26,"tag":69,"props":70,"children":71},"th",{},[72],{"type":31,"value":73},"Method",{"type":26,"tag":69,"props":75,"children":76},{},[77],{"type":31,"value":78},"File size",{"type":26,"tag":69,"props":80,"children":81},{},[82],{"type":31,"value":83},"Selectable text",{"type":26,"tag":69,"props":85,"children":86},{},[87],{"type":31,"value":88},"Row cut at page break",{"type":26,"tag":90,"props":91,"children":92},"tbody",{},[93,132,160,189],{"type":26,"tag":65,"props":94,"children":95},{},[96,108,116,124],{"type":26,"tag":97,"props":98,"children":99},"td",{},[100,106],{"type":26,"tag":101,"props":102,"children":103},"strong",{},[104],{"type":31,"value":105},"PDFShift API",{"type":31,"value":107},", same HTML",{"type":26,"tag":97,"props":109,"children":110},{},[111],{"type":26,"tag":101,"props":112,"children":113},{},[114],{"type":31,"value":115},"64 KB",{"type":26,"tag":97,"props":117,"children":118},{},[119],{"type":26,"tag":101,"props":120,"children":121},{},[122],{"type":31,"value":123},"Yes, 3,455 characters",{"type":26,"tag":97,"props":125,"children":126},{},[127],{"type":26,"tag":101,"props":128,"children":129},{},[130],{"type":31,"value":131},"No",{"type":26,"tag":65,"props":133,"children":134},{},[135,146,151,156],{"type":26,"tag":97,"props":136,"children":137},{},[138,144],{"type":26,"tag":46,"props":139,"children":141},{"className":140},[],[142],{"type":31,"value":143},"renderToStaticMarkup",{"type":31,"value":145}," + Puppeteer",{"type":26,"tag":97,"props":147,"children":148},{},[149],{"type":31,"value":150},"103 KB",{"type":26,"tag":97,"props":152,"children":153},{},[154],{"type":31,"value":155},"Yes, 3,451 characters",{"type":26,"tag":97,"props":157,"children":158},{},[159],{"type":31,"value":131},{"type":26,"tag":65,"props":161,"children":162},{},[163,168,173,178],{"type":26,"tag":97,"props":164,"children":165},{},[166],{"type":31,"value":167},"@react-pdf/renderer",{"type":26,"tag":97,"props":169,"children":170},{},[171],{"type":31,"value":172},"7.5 KB",{"type":26,"tag":97,"props":174,"children":175},{},[176],{"type":31,"value":177},"Yes, 3,350 characters",{"type":26,"tag":97,"props":179,"children":180},{},[181,183],{"type":31,"value":182},"Yes, row 24, until we added ",{"type":26,"tag":46,"props":184,"children":186},{"className":185},[],[187],{"type":31,"value":188},"wrap={false}",{"type":26,"tag":65,"props":190,"children":191},{},[192,197,202,207],{"type":26,"tag":97,"props":193,"children":194},{},[195],{"type":31,"value":196},"react-to-pdf (html2canvas + jsPDF)",{"type":26,"tag":97,"props":198,"children":199},{},[200],{"type":31,"value":201},"1,744 KB",{"type":26,"tag":97,"props":203,"children":204},{},[205],{"type":31,"value":206},"No, 2 characters",{"type":26,"tag":97,"props":208,"children":209},{},[210],{"type":31,"value":211},"Cuts at a fixed height",{"type":26,"tag":213,"props":214,"children":216},"guide-figure",{"caption":215},"PDF file size for the same 45-row React invoice, four methods. Our test, November 10, 2026.",[217],{"type":26,"tag":218,"props":219,"children":228},"svg",{"viewBox":220,"xmlns":221,"role":222,"ariaLabelledBy":223,"style":226,"fontFamily":227},"0 0 360 262","http://www.w3.org/2000/svg","img",[224,225],"r2p-size-title","r2p-size-desc","max-width:360px;display:block;margin:0 auto","DM Sans, sans-serif",[229,235,241,261,310,318,323,329,335],{"type":26,"tag":230,"props":231,"children":232},"title",{"id":224},[233],{"type":31,"value":234},"react-to-pdf's file is 17 to 232 times bigger than the others",{"type":26,"tag":236,"props":237,"children":238},"desc",{"id":225},[239],{"type":31,"value":240},"Horizontal bar chart of file size in KB. PDFShift API 64 KB. renderToStaticMarkup plus Puppeteer 103 KB. @react-pdf/renderer 7.5 KB. react-to-pdf 1,744 KB, with no selectable text.",{"type":26,"tag":242,"props":243,"children":245},"g",{"stroke":244},"#DBE4F2",[246,253,257],{"type":26,"tag":247,"props":248,"children":252},"line",{"x1":249,"y1":250,"x2":249,"y2":251},"10","4","232",[],{"type":26,"tag":247,"props":254,"children":256},{"x1":255,"y1":250,"x2":255,"y2":251},"170",[],{"type":26,"tag":247,"props":258,"children":260},{"x1":259,"y1":250,"x2":259,"y2":251},"330",[],{"type":26,"tag":242,"props":262,"children":265},{"style":263,"fill":264},"font-size:13px","#4C556D",[266,273,279,286,293,299,304],{"type":26,"tag":31,"props":267,"children":270},{"x":249,"y":268,"textAnchor":269},"254","start",[271],{"type":31,"value":272},"0",{"type":26,"tag":31,"props":274,"children":276},{"x":255,"y":268,"textAnchor":275},"middle",[277],{"type":31,"value":278},"1,000",{"type":26,"tag":31,"props":280,"children":283},{"x":281,"y":268,"textAnchor":282},"350","end",[284],{"type":31,"value":285},"2,000 KB",{"type":26,"tag":31,"props":287,"children":291},{"x":288,"y":288,"fill":289,"style":290},"18","#0A0C3D","font-weight:600",[292],{"type":31,"value":105},{"type":26,"tag":31,"props":294,"children":296},{"x":288,"y":295},"76",[297],{"type":31,"value":298},"renderToStaticMarkup + Puppeteer",{"type":26,"tag":31,"props":300,"children":302},{"x":288,"y":301},"134",[303],{"type":31,"value":167},{"type":26,"tag":31,"props":305,"children":307},{"x":288,"y":306},"192",[308],{"type":31,"value":309},"react-to-pdf",{"type":26,"tag":311,"props":312,"children":317},"rect",{"x":249,"y":313,"width":249,"height":314,"rx":315,"fill":316},"26","22","3","#6C47FF",[],{"type":26,"tag":311,"props":319,"children":322},{"x":249,"y":320,"width":321,"height":314,"rx":315,"fill":264},"84","17",[],{"type":26,"tag":311,"props":324,"children":328},{"x":249,"y":325,"width":326,"height":314,"rx":327,"fill":264},"142","2","1",[],{"type":26,"tag":311,"props":330,"children":334},{"x":249,"y":331,"width":332,"height":314,"rx":315,"fill":333},"200","279","#FF9D28",[],{"type":26,"tag":242,"props":336,"children":337},{"style":263,"fill":289},[338,344,350,356],{"type":26,"tag":31,"props":339,"children":342},{"x":340,"y":341,"style":290},"28","42",[343],{"type":31,"value":115},{"type":26,"tag":31,"props":345,"children":348},{"x":346,"y":347},"35","100",[349],{"type":31,"value":150},{"type":26,"tag":31,"props":351,"children":354},{"x":352,"y":353},"20","158",[355],{"type":31,"value":172},{"type":26,"tag":31,"props":357,"children":360},{"x":358,"y":359,"textAnchor":282},"281","216",[361],{"type":31,"value":362},"1,744 KB, no text",{"type":26,"tag":34,"props":364,"children":366},{"id":365},"why-is-the-react-to-pdf-file-so-big-and-why-cant-i-copy-its-text",[367],{"type":31,"value":368},"Why is the react-to-pdf file so big, and why can't I copy its text?",{"type":26,"tag":27,"props":370,"children":371},{},[372,374,383],{"type":31,"value":373},"Because it's a picture. react-to-pdf draws your component onto a canvas with html2canvas, saves it as JPEG, and puts the image on PDF pages with jsPDF. ",{"type":26,"tag":375,"props":376,"children":380},"a",{"href":377,"rel":378},"https://www.npmjs.com/package/react-to-pdf",[379],"nofollow",[381],{"type":31,"value":382},"Its README",{"type":31,"value":384}," says so: \"Not vectorized - the pdf is created from a screenshot of the component\". Nobody can search it, copy an amount from it, or read it with a screen reader.",{"type":26,"tag":213,"props":386,"children":388},{"caption":387},"Characters of text pdf.js could extract from each PDF of the same invoice. Our test, November 10, 2026.",[389],{"type":26,"tag":218,"props":390,"children":394},{"viewBox":220,"xmlns":221,"role":222,"ariaLabelledBy":391,"style":226,"fontFamily":227},[392,393],"r2p-text-title","r2p-text-desc",[395,400,405,417,450,454,457,461,464,485],{"type":26,"tag":230,"props":396,"children":397},{"id":392},[398],{"type":31,"value":399},"react-to-pdf's PDF has 2 characters of text, the others about 3,400",{"type":26,"tag":236,"props":401,"children":402},{"id":393},[403],{"type":31,"value":404},"Horizontal bar chart of characters extracted from each PDF. PDFShift API 3,455. renderToStaticMarkup plus Puppeteer 3,451. @react-pdf/renderer 3,350. react-to-pdf 2.",{"type":26,"tag":242,"props":406,"children":407},{"stroke":244},[408,411,414],{"type":26,"tag":247,"props":409,"children":410},{"x1":249,"y1":250,"x2":249,"y2":251},[],{"type":26,"tag":247,"props":412,"children":413},{"x1":255,"y1":250,"x2":255,"y2":251},[],{"type":26,"tag":247,"props":415,"children":416},{"x1":259,"y1":250,"x2":259,"y2":251},[],{"type":26,"tag":242,"props":418,"children":419},{"style":263,"fill":264},[420,424,429,434,438,442,446],{"type":26,"tag":31,"props":421,"children":422},{"x":249,"y":268,"textAnchor":269},[423],{"type":31,"value":272},{"type":26,"tag":31,"props":425,"children":426},{"x":255,"y":268,"textAnchor":275},[427],{"type":31,"value":428},"2,000",{"type":26,"tag":31,"props":430,"children":431},{"x":281,"y":268,"textAnchor":282},[432],{"type":31,"value":433},"4,000 characters",{"type":26,"tag":31,"props":435,"children":436},{"x":288,"y":288,"fill":289,"style":290},[437],{"type":31,"value":105},{"type":26,"tag":31,"props":439,"children":440},{"x":288,"y":295},[441],{"type":31,"value":298},{"type":26,"tag":31,"props":443,"children":444},{"x":288,"y":301},[445],{"type":31,"value":167},{"type":26,"tag":31,"props":447,"children":448},{"x":288,"y":306},[449],{"type":31,"value":309},{"type":26,"tag":311,"props":451,"children":453},{"x":249,"y":313,"width":452,"height":314,"rx":315,"fill":316},"276",[],{"type":26,"tag":311,"props":455,"children":456},{"x":249,"y":320,"width":452,"height":314,"rx":315,"fill":264},[],{"type":26,"tag":311,"props":458,"children":460},{"x":249,"y":325,"width":459,"height":314,"rx":315,"fill":264},"268",[],{"type":26,"tag":311,"props":462,"children":463},{"x":249,"y":331,"width":326,"height":314,"rx":327,"fill":333},[],{"type":26,"tag":242,"props":465,"children":467},{"style":263,"fill":466},"#FFFFFF",[468,474,479],{"type":26,"tag":31,"props":469,"children":471},{"x":470,"y":341,"textAnchor":282,"style":290},"278",[472],{"type":31,"value":473},"3,455",{"type":26,"tag":31,"props":475,"children":476},{"x":470,"y":347,"textAnchor":282},[477],{"type":31,"value":478},"3,451",{"type":26,"tag":31,"props":480,"children":482},{"x":481,"y":353,"textAnchor":282},"270",[483],{"type":31,"value":484},"3,350",{"type":26,"tag":31,"props":486,"children":487},{"x":352,"y":359,"style":263,"fill":289},[488],{"type":31,"value":326},{"type":26,"tag":490,"props":491,"children":496},"pre",{"className":492,"code":494,"language":495,"meta":7},[493],"language-jsx","import { usePDF } from 'react-to-pdf';\n\nexport function DownloadInvoice() {\n    const { toPDF, targetRef } = usePDF({ filename: 'invoice.pdf' });\n    return (\n        \u003C>\n            \u003Cbutton onClick={() => toPDF()}>Download PDF\u003C/button>\n            \u003Cdiv ref={targetRef}>\u003CInvoice />\u003C/div>\n        \u003C/>\n    );\n}\n","jsx",[497],{"type":26,"tag":46,"props":498,"children":499},{"__ignoreMap":7},[500],{"type":31,"value":494},{"type":26,"tag":27,"props":502,"children":503},{},[504],{"type":31,"value":505},"It looked like the browser because it is a picture of the browser. But the image is cut into pages at a fixed height, not between rows. Ours happened to break between rows 44 and 45; a longer description would have sliced a row through its text. The output also changes with the window width, since that's what gets captured. And it only runs in a browser: the README says \"No SSR\".",{"type":26,"tag":27,"props":507,"children":508},{},[509],{"type":31,"value":510},"Use it for a one-page card or chart a user downloads from the screen. Don't use it for invoices, contracts or anything someone needs to search.",{"type":26,"tag":34,"props":512,"children":514},{"id":513},"is-react-pdfrenderer-worth-rewriting-your-component",[515],{"type":31,"value":516},"Is @react-pdf/renderer worth rewriting your component?",{"type":26,"tag":27,"props":518,"children":519},{},[520,522,528,530,536,537,543,544,550,552,558,560,567,569,575,577,583,585,591],{"type":31,"value":521},"Only if the PDF is a new document. @react-pdf/renderer made the smallest, fastest file in our test, 7.5 KB in a median 33 ms, but it doesn't render HTML. You write the document again with its own primitives (",{"type":26,"tag":46,"props":523,"children":525},{"className":524},[],[526],{"type":31,"value":527},"Document",{"type":31,"value":529},", ",{"type":26,"tag":46,"props":531,"children":533},{"className":532},[],[534],{"type":31,"value":535},"Page",{"type":31,"value":529},{"type":26,"tag":46,"props":538,"children":540},{"className":539},[],[541],{"type":31,"value":542},"View",{"type":31,"value":529},{"type":26,"tag":46,"props":545,"children":547},{"className":546},[],[548],{"type":31,"value":549},"Text",{"type":31,"value":551},") and a ",{"type":26,"tag":46,"props":553,"children":555},{"className":554},[],[556],{"type":31,"value":557},"StyleSheet",{"type":31,"value":559},", and its styling is Flexbox only: the ",{"type":26,"tag":375,"props":561,"children":564},{"href":562,"rel":563},"https://react-pdf.org/styling",[379],[565],{"type":31,"value":566},"styling docs",{"type":31,"value":568}," list ",{"type":26,"tag":46,"props":570,"children":572},{"className":571},[],[573],{"type":31,"value":574},"display: flex",{"type":31,"value":576}," and ",{"type":26,"tag":46,"props":578,"children":580},{"className":579},[],[581],{"type":31,"value":582},"none",{"type":31,"value":584},", with no grid. Units are points by default. Custom fonts must be TTF or WOFF files registered with ",{"type":26,"tag":46,"props":586,"children":588},{"className":587},[],[589],{"type":31,"value":590},"Font.register",{"type":31,"value":592},".",{"type":26,"tag":27,"props":594,"children":595},{},[596,598,603],{"type":31,"value":597},"Our port was 48 lines against the original's 46. The grid became two flex columns, and the table became rows of ",{"type":26,"tag":46,"props":599,"children":601},{"className":600},[],[602],{"type":31,"value":542},{"type":31,"value":604},"s:",{"type":26,"tag":490,"props":606,"children":609},{"className":607,"code":608,"language":495,"meta":7},[493],"import { renderToBuffer, Document, Page, View, Text } from '@react-pdf/renderer';\n\nconst InvoicePdf = () => (\n    \u003CDocument>\n        \u003CPage size=\"A4\" style={s.page}>\n            {/* header, parties and table head as View and Text */}\n            {items.map(i => (\n                // wrap={false} keeps each row whole across a page break\n                \u003CView style={s.row} wrap={false} key={i.id}>\n                    \u003CText style={s.desc}>{i.desc}\u003C/Text>\n                    \u003CText style={s.num}>{i.qty}\u003C/Text>\n                \u003C/View>\n            ))}\n        \u003C/Page>\n    \u003C/Document>\n);\n\nconst buffer = await renderToBuffer(\u003CInvoicePdf />); // a Node Buffer, ready to save or send\n",[610],{"type":26,"tag":46,"props":611,"children":612},{"__ignoreMap":7},[613],{"type":31,"value":608},{"type":26,"tag":27,"props":615,"children":616},{},[617,619,624],{"type":31,"value":618},"Without ",{"type":26,"tag":46,"props":620,"children":622},{"className":621},[],[623],{"type":31,"value":188},{"type":31,"value":625},", row 24 split across the page break. With it, no row split.",{"type":26,"tag":27,"props":627,"children":628},{},[629],{"type":31,"value":630},"And as long as the web version exists, every change to the invoice gets made twice.",{"type":26,"tag":34,"props":632,"children":634},{"id":633},"how-do-you-render-an-existing-react-component-to-pdf-on-the-server",[635],{"type":31,"value":636},"How do you render an existing React component to PDF on the server?",{"type":26,"tag":27,"props":638,"children":639},{},[640,642,647,649,655],{"type":31,"value":641},"Turn the component into an HTML string with ",{"type":26,"tag":46,"props":643,"children":645},{"className":644},[],[646],{"type":31,"value":143},{"type":31,"value":648}," from ",{"type":26,"tag":46,"props":650,"children":652},{"className":651},[],[653],{"type":31,"value":654},"react-dom/server",{"type":31,"value":656},", put it in a full HTML document, and print that with headless Chromium. Same component, same CSS, no rewrite. This is what we ran:",{"type":26,"tag":490,"props":658,"children":661},{"className":659,"code":660,"language":495,"meta":7},[493],"import { renderToStaticMarkup } from 'react-dom/server';\nimport puppeteer from 'puppeteer';\n\nconst html = `\u003C!doctype html>\u003Chtml>\u003Chead>\u003Cmeta charset=\"utf-8\">\u003C/head>\n\u003Cbody style=\"margin:0\">${renderToStaticMarkup(\u003CInvoice />)}\u003C/body>\u003C/html>`;\n\nconst browser = await puppeteer.launch();\nconst page = await browser.newPage();\nawait page.setContent(html);\nconst pdf = await page.pdf({ format: 'A4', printBackground: true });\nawait browser.close();\n",[662],{"type":26,"tag":46,"props":663,"children":664},{"__ignoreMap":7},[665],{"type":31,"value":660},{"type":26,"tag":27,"props":667,"children":668},{},[669,671,676,678,684,686,692],{"type":31,"value":670},"The PDF kept the grid and the flex header, broke between rows, and repeated the table's header row at the top of page 2. Use a real ",{"type":26,"tag":46,"props":672,"children":674},{"className":673},[],[675],{"type":31,"value":51},{"type":31,"value":677}," and you get that for free. Keep ",{"type":26,"tag":46,"props":679,"children":681},{"className":680},[],[682],{"type":31,"value":683},"printBackground: true",{"type":31,"value":685},". Puppeteer defaults to ",{"type":26,"tag":46,"props":687,"children":689},{"className":688},[],[690],{"type":31,"value":691},"false",{"type":31,"value":693},", which drops background colors.",{"type":26,"tag":27,"props":695,"children":696},{},[697,699,705,707,713,715,720,722,729,731,737],{"type":31,"value":698},"Two gotchas. The HTML string has no styles unless your component brings them, so put your CSS in the document's ",{"type":26,"tag":46,"props":700,"children":702},{"className":701},[],[703],{"type":31,"value":704},"\u003Chead>",{"type":31,"value":706}," or in a ",{"type":26,"tag":46,"props":708,"children":710},{"className":709},[],[711],{"type":31,"value":712},"\u003Cstyle>",{"type":31,"value":714}," tag. And ",{"type":26,"tag":46,"props":716,"children":718},{"className":717},[],[719],{"type":31,"value":143},{"type":31,"value":721}," doesn't wait for data: if a component suspends, React ",{"type":26,"tag":375,"props":723,"children":726},{"href":724,"rel":725},"https://react.dev/reference/react-dom/server/renderToStaticMarkup",[379],[727],{"type":31,"value":728},"sends its fallback",{"type":31,"value":730}," instead. Load the data first and pass it in as props, or use React 19's ",{"type":26,"tag":46,"props":732,"children":734},{"className":733},[],[735],{"type":31,"value":736},"prerenderToNodeStream",{"type":31,"value":738},", which waits for it.",{"type":26,"tag":27,"props":740,"children":741},{},[742,744,750],{"type":31,"value":743},"Puppeteer took 67 ms with the browser already open, and launching the browser took 404 ms more. And now you run Chromium on your servers, with its memory, its crashes and its updates. Our ",{"type":26,"tag":375,"props":745,"children":747},{"href":746},"/guides/puppeteer-pdf-in-production/",[748],{"type":31,"value":749},"Puppeteer production guide",{"type":31,"value":751}," covers what that takes.",{"type":26,"tag":34,"props":753,"children":755},{"id":754},"can-you-send-that-html-string-to-an-api-instead-of-running-chromium",[756],{"type":31,"value":757},"Can you send that HTML string to an API instead of running Chromium?",{"type":26,"tag":27,"props":759,"children":760},{},[761,763,769,771,777],{"type":31,"value":762},"Yes. The ",{"type":26,"tag":46,"props":764,"children":766},{"className":765},[],[767],{"type":31,"value":768},"html",{"type":31,"value":770}," string from the last step is all the ",{"type":26,"tag":375,"props":772,"children":775},{"href":773,"rel":774},"https://docs.pdfshift.io/api-reference/convert-to-pdf",[379],[776],{"type":31,"value":105},{"type":31,"value":778}," needs. We render it on our Chromium and send the PDF back:",{"type":26,"tag":490,"props":780,"children":785},{"className":781,"code":783,"language":784,"meta":7},[782],"language-js","const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {\n    method: 'POST',\n    headers: {\n        'X-API-Key': process.env.PDFSHIFT_API_KEY,\n        'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ source: html, format: 'A4' }),\n});\n\nif (!response.ok) {\n    throw new Error(`PDFShift returned ${response.status}: ${await response.text()}`);\n}\n\nconst pdf = Buffer.from(await response.arrayBuffer());\n","js",[786],{"type":26,"tag":46,"props":787,"children":788},{"__ignoreMap":7},[789],{"type":31,"value":783},{"type":26,"tag":27,"props":791,"children":792},{},[793,795,801,803,809],{"type":31,"value":794},"In a Next.js Route Handler, return it with ",{"type":26,"tag":46,"props":796,"children":798},{"className":797},[],[799],{"type":31,"value":800},"new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } })",{"type":31,"value":802},". If your component loads something after the first paint, the ",{"type":26,"tag":46,"props":804,"children":806},{"className":805},[],[807],{"type":31,"value":808},"wait_for",{"type":31,"value":810}," parameter names a global function we wait on before printing, for up to 30 seconds.",{"type":26,"tag":27,"props":812,"children":813},{},[814],{"type":31,"value":815},"Our invoice came back as two pages with every row whole and every word selectable: the same result as Puppeteer, without a browser to install, patch or keep alive. People don't buy PDFs from us. They buy not having to run Chromium.",{"type":26,"tag":27,"props":817,"children":818},{},[819,821,827],{"type":31,"value":820},"The free plan has 50 credits a month with no credit card. ",{"type":26,"tag":375,"props":822,"children":824},{"href":823},"/register/",[825],{"type":31,"value":826},"Get a free API key",{"type":31,"value":828}," and send your own component through it.",{"type":26,"tag":34,"props":830,"children":832},{"id":831},"how-we-tested",[833],{"type":31,"value":834},"How we tested",{"type":26,"tag":27,"props":836,"children":837},{},[838,840,846],{"type":31,"value":839},"One machine, November 10, 2026: Apple Silicon, macOS 27.0.1, Node 26.11.1. React and react-dom 19.3.1, @react-pdf/renderer 4.9.0, react-to-pdf 3.2.2 (with html2canvas 1.4.1 and jsPDF 4.2.1), Puppeteer 25.17.0 with Chrome for Testing 157. The web component (",{"type":26,"tag":46,"props":841,"children":843},{"className":842},[],[844],{"type":31,"value":845},"Invoice.jsx",{"type":31,"value":847},", 46 lines) and its @react-pdf/renderer port (48 lines) read the same 45-item data file.",{"type":26,"tag":849,"props":850,"children":851},"ul",{},[852,862,896,906],{"type":26,"tag":853,"props":854,"children":855},"li",{},[856,860],{"type":26,"tag":101,"props":857,"children":858},{},[859],{"type":31,"value":309},{"type":31,"value":861}," ran in headless Chrome at a 1,000 px wide window, with its default options (A4, JPEG, resolution 3).",{"type":26,"tag":853,"props":863,"children":864},{},[865,870,872,878,880,886,888,894],{"type":26,"tag":101,"props":866,"children":867},{},[868],{"type":31,"value":869},"Puppeteer",{"type":31,"value":871}," timings are ",{"type":26,"tag":46,"props":873,"children":875},{"className":874},[],[876],{"type":31,"value":877},"setContent",{"type":31,"value":879}," plus ",{"type":26,"tag":46,"props":881,"children":883},{"className":882},[],[884],{"type":31,"value":885},"page.pdf()",{"type":31,"value":887},", with the browser already launched. Chrome needed the ",{"type":26,"tag":46,"props":889,"children":891},{"className":890},[],[892],{"type":31,"value":893},"--no-sandbox",{"type":31,"value":895}," flag on our test machine to load the page; keep the sandbox on for HTML your users write.",{"type":26,"tag":853,"props":897,"children":898},{},[899,904],{"type":26,"tag":101,"props":900,"children":901},{},[902],{"type":31,"value":903},"PDFShift",{"type":31,"value":905}," got the same HTML string. We called it without an API key, so that file has the watermark keyless calls get.",{"type":26,"tag":853,"props":907,"children":908},{},[909],{"type":31,"value":910},"Each time is the median of 3 runs. Text was counted with pdf.js, and we checked the page break in the @react-pdf/renderer and Puppeteer files by rendering page 2 to an image and looking at it.",{"type":26,"tag":34,"props":912,"children":914},{"id":913},"frequently-asked-questions",[915],{"type":31,"value":916},"Frequently asked questions",{"type":26,"tag":918,"props":919,"children":921},"h3",{"id":920},"is-react-pdf-the-same-package-as-react-pdfrenderer",[922],{"type":31,"value":14},{"type":26,"tag":27,"props":924,"children":925},{},[926,928,933,934,939,940,945,946,951],{"type":31,"value":927},"No. @react-pdf/renderer creates PDFs from its own components (",{"type":26,"tag":46,"props":929,"children":931},{"className":930},[],[932],{"type":31,"value":527},{"type":31,"value":529},{"type":26,"tag":46,"props":935,"children":937},{"className":936},[],[938],{"type":31,"value":535},{"type":31,"value":529},{"type":26,"tag":46,"props":941,"children":943},{"className":942},[],[944],{"type":31,"value":542},{"type":31,"value":529},{"type":26,"tag":46,"props":947,"children":949},{"className":948},[],[950],{"type":31,"value":549},{"type":31,"value":952},"). The package named react-pdf displays existing PDFs inside a React app. Install the first to generate a PDF, the second to show one.",{"type":26,"tag":918,"props":954,"children":956},{"id":955},"what-about-react-to-print",[957],{"type":31,"value":17},{"type":26,"tag":27,"props":959,"children":960},{},[961],{"type":31,"value":18},{"type":26,"tag":918,"props":963,"children":965},{"id":964},"can-i-generate-a-pdf-in-the-browser-without-taking-a-screenshot",[966],{"type":31,"value":20},{"type":26,"tag":27,"props":968,"children":969},{},[970,972,978,979,985],{"type":31,"value":971},"Yes, with @react-pdf/renderer, which runs in the browser as well as in Node. Its ",{"type":26,"tag":46,"props":973,"children":975},{"className":974},[],[976],{"type":31,"value":977},"PDFDownloadLink",{"type":31,"value":576},{"type":26,"tag":46,"props":980,"children":982},{"className":981},[],[983],{"type":31,"value":984},"BlobProvider",{"type":31,"value":986}," components build a real PDF with selectable text on the user's device.",{"title":7,"searchDepth":988,"depth":988,"links":989},2,[990,991,992,993,994,995,996],{"id":36,"depth":988,"text":39},{"id":365,"depth":988,"text":368},{"id":513,"depth":988,"text":516},{"id":633,"depth":988,"text":636},{"id":754,"depth":988,"text":757},{"id":831,"depth":988,"text":834},{"id":913,"depth":988,"text":916,"children":997},[998,1000,1001],{"id":920,"depth":999,"text":14},3,{"id":955,"depth":999,"text":17},{"id":964,"depth":999,"text":20},"markdown","content:guides:react-to-pdf.md","content","guides/react-to-pdf.md","md",1794479140939]