Refactor image rendering to use a browser pool and update dependencies

This commit is contained in:
ImBenji
2025-12-31 18:23:15 +00:00
parent 410a78ab2b
commit f876b01529
5 changed files with 130 additions and 36 deletions

3
.env.example Normal file
View File

@@ -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

34
api.js
View File

@@ -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) {
</body>
</html>`;
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&timestamp=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&timestamp=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);
});

108
browserPool.js Normal file
View File

@@ -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
};

View File

@@ -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('</head>', `${configScript}</head>`);
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 };

View File

@@ -13,7 +13,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"node-html-to-image": "^5.0.0",
"puppeteer": "^23.0.0",
"express": "^4.18.2"
}
}