Refactor image rendering to use a browser pool and update dependencies
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal 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
34
api.js
@@ -1,8 +1,8 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const nodeHtmlToImage = require('node-html-to-image');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const { initPool, renderHtml, POOL_SIZE } = require('./browserPool');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
@@ -262,20 +262,7 @@ async function generateQuoteBuffer(config) {
|
|||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
const image = await nodeHtmlToImage({
|
const image = await renderHtml(html, 1000);
|
||||||
html: html,
|
|
||||||
puppeteerArgs: {
|
|
||||||
args: ['--no-sandbox'],
|
|
||||||
defaultViewport: {
|
|
||||||
width: 3240,
|
|
||||||
height: 3240
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeScreenshot: async (page) => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,9 +387,16 @@ setInterval(() => {
|
|||||||
cleanupOldCache();
|
cleanupOldCache();
|
||||||
}, 60 * 60 * 1000);
|
}, 60 * 60 * 1000);
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
// Initialize browser pool then start server
|
||||||
console.log(`Quote generator API running on http://localhost:${PORT}`);
|
initPool().then(() => {
|
||||||
console.log(`GET: http://localhost:${PORT}/generate?text=Hello&displayName=Test&username=@test×tamp=1735574400`);
|
app.listen(PORT, () => {
|
||||||
console.log(`POST: http://localhost:${PORT}/generate`);
|
console.log(`Quote generator API running on http://localhost:${PORT}`);
|
||||||
console.log(`Cache cleared on startup, cleanup runs every hour`);
|
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);
|
||||||
});
|
});
|
||||||
|
|||||||
108
browserPool.js
Normal file
108
browserPool.js
Normal 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
|
||||||
|
};
|
||||||
19
generate.js
19
generate.js
@@ -1,6 +1,6 @@
|
|||||||
const nodeHtmlToImage = require('node-html-to-image');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { renderHtml, shutdownPool } = require('./browserPool');
|
||||||
|
|
||||||
async function generateQuote(config, outputPath = 'quote.png') {
|
async function generateQuote(config, outputPath = 'quote.png') {
|
||||||
const htmlTemplate = fs.readFileSync(path.join(__dirname, 'quote.html'), 'utf8');
|
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>`);
|
const modifiedHtml = htmlTemplate.replace('</head>', `${configScript}</head>`);
|
||||||
|
|
||||||
await nodeHtmlToImage({
|
const imageBuffer = await renderHtml(modifiedHtml, 500);
|
||||||
output: outputPath,
|
fs.writeFileSync(outputPath, imageBuffer);
|
||||||
html: modifiedHtml,
|
|
||||||
puppeteerArgs: {
|
|
||||||
args: ['--no-sandbox'],
|
|
||||||
defaultViewport: {
|
|
||||||
width: 3240,
|
|
||||||
height: 3240
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeScreenshot: async (page) => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Quote generated: ${outputPath}`);
|
console.log(`Quote generated: ${outputPath}`);
|
||||||
}
|
}
|
||||||
@@ -57,6 +45,7 @@ const exampleTweetWithImage = {
|
|||||||
(async () => {
|
(async () => {
|
||||||
await generateQuote(exampleTweet, 'quote-no-image.png');
|
await generateQuote(exampleTweet, 'quote-no-image.png');
|
||||||
await generateQuote(exampleTweetWithImage, 'quote-with-image.png');
|
await generateQuote(exampleTweetWithImage, 'quote-with-image.png');
|
||||||
|
await shutdownPool();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
module.exports = { generateQuote };
|
module.exports = { generateQuote };
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-html-to-image": "^5.0.0",
|
"puppeteer": "^23.0.0",
|
||||||
"express": "^4.18.2"
|
"express": "^4.18.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user