From f876b015296bc76b0f14ec7a8f876c3292b76a2c Mon Sep 17 00:00:00 2001 From: ImBenji Date: Wed, 31 Dec 2025 18:23:15 +0000 Subject: [PATCH] Refactor image rendering to use a browser pool and update dependencies --- .env.example | 3 ++ api.js | 34 +++++++--------- browserPool.js | 108 +++++++++++++++++++++++++++++++++++++++++++++++++ generate.js | 19 ++------- package.json | 2 +- 5 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 .env.example create mode 100644 browserPool.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..84f57be --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Number of browser instances to keep in the pool +# More instances = more concurrent requests but more memory usage +BROWSER_POOL_SIZE=5 diff --git a/api.js b/api.js index f999101..0d0c2c1 100644 --- a/api.js +++ b/api.js @@ -1,8 +1,8 @@ const express = require('express'); -const nodeHtmlToImage = require('node-html-to-image'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const { initPool, renderHtml, POOL_SIZE } = require('./browserPool'); const app = express(); const PORT = 3000; @@ -262,20 +262,7 @@ async function generateQuoteBuffer(config) { `; - const image = await nodeHtmlToImage({ - html: html, - puppeteerArgs: { - args: ['--no-sandbox'], - defaultViewport: { - width: 3240, - height: 3240 - } - }, - beforeScreenshot: async (page) => { - await new Promise(resolve => setTimeout(resolve, 1000)); - } - }); - + const image = await renderHtml(html, 1000); return image; } @@ -400,9 +387,16 @@ setInterval(() => { cleanupOldCache(); }, 60 * 60 * 1000); -app.listen(PORT, () => { - console.log(`Quote generator API running on http://localhost:${PORT}`); - console.log(`GET: http://localhost:${PORT}/generate?text=Hello&displayName=Test&username=@test×tamp=1735574400`); - console.log(`POST: http://localhost:${PORT}/generate`); - console.log(`Cache cleared on startup, cleanup runs every hour`); +// Initialize browser pool then start server +initPool().then(() => { + app.listen(PORT, () => { + console.log(`Quote generator API running on http://localhost:${PORT}`); + console.log(`Browser pool size: ${POOL_SIZE} (set BROWSER_POOL_SIZE env var to change)`); + console.log(`GET: http://localhost:${PORT}/generate?text=Hello&displayName=Test&username=@test×tamp=1735574400`); + console.log(`POST: http://localhost:${PORT}/generate`); + console.log(`Cache cleared on startup, cleanup runs every hour`); + }); +}).catch(err => { + console.error('Failed to initialize browser pool:', err); + process.exit(1); }); diff --git a/browserPool.js b/browserPool.js new file mode 100644 index 0000000..e813834 --- /dev/null +++ b/browserPool.js @@ -0,0 +1,108 @@ +const puppeteer = require('puppeteer'); + +const POOL_SIZE = parseInt(process.env.BROWSER_POOL_SIZE) || 5; +const VIEWPORT_WIDTH = 3240; +const VIEWPORT_HEIGHT = 3240; + +let browsers = []; +let availablePages = []; +let initPromise = null; + + +async function initPool() { + if (initPromise) return initPromise; + + initPromise = (async () => { + console.log(`Initializing browser pool with ${POOL_SIZE} instances...`); + + for (let i = 0; i < POOL_SIZE; i++) { + const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + browsers.push(browser); + + const page = await browser.newPage(); + await page.setViewport({ + width: VIEWPORT_WIDTH, + height: VIEWPORT_HEIGHT + }); + + availablePages.push(page); + } + + console.log(`Browser pool ready with ${POOL_SIZE} pages`); + })(); + + return initPromise; +} + +async function acquirePage() { + await initPool(); + + // wait for available page + while (availablePages.length === 0) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + + return availablePages.pop(); +} + +function releasePage(page) { + availablePages.push(page); +} + +// renders html and returns screenshot as buffer +async function renderHtml(html, waitTime = 1000) { + const page = await acquirePage(); + + try { + await page.setContent(html, { waitUntil: 'networkidle0' }); + + // wait for any animations or laoding + await new Promise(resolve => setTimeout(resolve, waitTime)); + + const screenshot = await page.screenshot({ + type: 'png', + fullPage: false + }); + + return screenshot; + } finally { + releasePage(page); + } +} + + +async function shutdownPool() { + console.log('Shutting down browser pool...'); + + for (const browser of browsers) { + await browser.close(); + } + + browsers = []; + availablePages = []; + initPromise = null; + + console.log('Browser pool shut down'); +} + +// Graceful shutdown handlers +process.on('SIGINT', async () => { + await shutdownPool(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + await shutdownPool(); + process.exit(0); +}); + +module.exports = { + initPool, + renderHtml, + shutdownPool, + POOL_SIZE +}; diff --git a/generate.js b/generate.js index 8c18581..4297251 100644 --- a/generate.js +++ b/generate.js @@ -1,6 +1,6 @@ -const nodeHtmlToImage = require('node-html-to-image'); const fs = require('fs'); const path = require('path'); +const { renderHtml, shutdownPool } = require('./browserPool'); async function generateQuote(config, outputPath = 'quote.png') { const htmlTemplate = fs.readFileSync(path.join(__dirname, 'quote.html'), 'utf8'); @@ -13,20 +13,8 @@ async function generateQuote(config, outputPath = 'quote.png') { const modifiedHtml = htmlTemplate.replace('', `${configScript}`); - await nodeHtmlToImage({ - output: outputPath, - html: modifiedHtml, - puppeteerArgs: { - args: ['--no-sandbox'], - defaultViewport: { - width: 3240, - height: 3240 - } - }, - beforeScreenshot: async (page) => { - await new Promise(resolve => setTimeout(resolve, 500)); - } - }); + const imageBuffer = await renderHtml(modifiedHtml, 500); + fs.writeFileSync(outputPath, imageBuffer); console.log(`Quote generated: ${outputPath}`); } @@ -57,6 +45,7 @@ const exampleTweetWithImage = { (async () => { await generateQuote(exampleTweet, 'quote-no-image.png'); await generateQuote(exampleTweetWithImage, 'quote-with-image.png'); + await shutdownPool(); })(); module.exports = { generateQuote }; diff --git a/package.json b/package.json index 4f9aa83..aa91849 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "license": "ISC", "type": "commonjs", "dependencies": { - "node-html-to-image": "^5.0.0", + "puppeteer": "^23.0.0", "express": "^4.18.2" } }