用 puppeteer 可以实现 ssr,但性能堪忧。在我的虚拟机里,即使是简单的页面也需要 1~2 秒才能渲染出来。如果有并发请求,渲染时间会乘以 n。因此,实用性几乎为零。
但是,其他的 ssr 方式又比较麻烦,并且不通用。所以我就考虑,是不是可以对 puppeteer 用作 ssr 的方案进行优化。基本思路如下:
- 添加页面缓存。如果页面在缓存之中,并且没有过期,直接用缓存。
- 添加任务队列。既然性能不好,那同一时间就只渲染指定数量的页面好了。建立一个请求数组,一个渲染实例数组。所有的请求暂存到请求数组里,所有的渲染实例暂存到渲染实例数组里。如果有请求并且可以渲染,就渲染最先进入的请求。
- 添加检查定时器。定时检查请求数组和渲染实例数组。
下面是代码:
ssr.js:
var puppeteer = require("puppeteer"); // In-memory cache of rendered pages. Note: this will be cleared whenever the // server process stops. If you need true persistence, use something like // Google Cloud Storage (https://firebase.google.com/docs/storage/web/start). const RENDER_CACHE = new Map(); // instance 是渲染实例数组 async function ssr(url, instance) { console.log("url", url); if (RENDER_CACHE.has(url)) { if (Date.now() - RENDER_CACHE.get(url).time < 3600 * 1000) { console.log('cached') return { html: RENDER_CACHE.get(url).html, ttRenderMs: 0 }; } } const start = Date.now(); instance.push(start); // 因为启动chrome需要时间,所以用开始时间占位 const browser = await puppeteer.launch({headless: true, args:['--no-sandbox']}); const page = await browser.newPage(); try { // networkidle0 waits for the network to be idle (no requests for 500ms). // The page's JS has likely produced markup by this point, but wait longer // if your site lazy loads, etc. await page.goto(url, { waitUntil: "networkidle0" }); } catch (err) { console.error(err); remove(instance, start); await browser.close(); throw new Error("page.goto/waitForSelector timed out."); } const html = await page.content(); // serialized HTML of page DOM. remove(instance, start); await browser.close(); const ttRenderMs = Date.now() - start; console.info(`Headless rendered page in: ${ttRenderMs}ms`); RENDER_CACHE.set(url, {html, time: Date.now()}); // cache rendered page. return { html, ttRenderMs }; } function remove(instance, ins) { instance.map((item, index) => { if (item === ins) { instance.splice(index, 1); } }); }
server.js:
const queue = []; checkLoop(2, queue, instance); app.use(express.static("./")); app.get("/*", async (req, res, next) => { queue.push({req, res, next}); }); function checkLoop(n, queue, instance) { setTimeout(() => { // console.info('check:' + queue.length + " " + instance.length) if (queue.length && instance.length < n) { renderPage(queue.shift(), instance); // console.info('render:' + queue.length) } checkLoop(n, queue, instance); }, 100); } async function renderPage(request, instance) { const { html, ttRenderMs } = await ssr( `${request.req.protocol}://shici.tony93-dev.top/`, // 我虚拟机里应用的地址,换成自己应用的地址。比如:`${request.req.protocol}://${request.req.host + request.req.url}` instance ); // Add Server-Timing! See https://w3c.github.io/server-timing/. request.res.set( "Server-Timing", `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"` ); return request.res.status(200).send(html); // Serve prerendered page as response. } app.listen(8080, () => console.log("Server started. Press Ctrl+C to quit"));