From d20fbbfe2e2769d62e43627b36e5a3707dbb36b8 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Fri, 20 Feb 2026 12:39:06 +0000 Subject: [PATCH] perf: switch to domcontentloaded default, optimize browser pool, fix swagger paths Performance fixes: - Default waitUntil changed from networkidle2 to domcontentloaded (saves ~500ms+) - Add waitUntil parameter so users can choose (load/domcontentloaded/networkidle0/networkidle2) - Optimize page recycle: use DOM reset instead of about:blank navigation - Add Chromium flags to disable unnecessary features (background networking, extensions, sync, etc.) Swagger fixes: - Fix apis glob to include dist/*.js (was only matching src/*.ts, empty at runtime) - Document new waitUntil parameter on POST /v1/screenshot - Add OpenAPI docs for /status endpoint --- src/docs/openapi.ts | 2 +- src/routes/screenshot.ts | 11 ++++++++++- src/routes/status.ts | 15 +++++++++++++++ src/services/browser.ts | 17 ++++++++++++++--- src/services/screenshot.ts | 4 +++- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/docs/openapi.ts b/src/docs/openapi.ts index da27cee..40025e0 100644 --- a/src/docs/openapi.ts +++ b/src/docs/openapi.ts @@ -47,7 +47,7 @@ const options: swaggerJsdoc.Options = { }, }, }, - apis: ["./src/routes/*.ts"], + apis: ["./src/routes/*.ts", "./dist/routes/*.js"], }; export const openapiSpec = swaggerJsdoc(options); diff --git a/src/routes/screenshot.ts b/src/routes/screenshot.ts index aa735e5..e3fcc67 100644 --- a/src/routes/screenshot.ts +++ b/src/routes/screenshot.ts @@ -70,6 +70,14 @@ export const screenshotRouter = Router(); * maximum: 5000 * default: 0 * description: Extra delay in ms after page load before capturing + * waitUntil: + * type: string + * enum: [load, domcontentloaded, networkidle0, networkidle2] + * default: domcontentloaded + * description: > + * Page load event to wait for before capturing. + * "domcontentloaded" (default) is fastest for most pages. + * Use "networkidle2" for JS-heavy SPAs that load data after initial render. * examples: * simple: * summary: Simple screenshot @@ -122,7 +130,7 @@ export const screenshotRouter = Router(); * schema: { $ref: "#/components/schemas/Error" } */ screenshotRouter.post("/", async (req: any, res) => { - const { url, format, width, height, fullPage, quality, waitForSelector, deviceScale, delay } = req.body; + const { url, format, width, height, fullPage, quality, waitForSelector, deviceScale, delay, waitUntil } = req.body; if (!url || typeof url !== "string") { res.status(400).json({ error: "Missing required parameter: url" }); @@ -140,6 +148,7 @@ screenshotRouter.post("/", async (req: any, res) => { waitForSelector, deviceScale: deviceScale ? parseFloat(deviceScale) : undefined, delay: delay ? parseInt(delay, 10) : undefined, + waitUntil: ["load", "domcontentloaded", "networkidle0", "networkidle2"].includes(waitUntil) ? waitUntil : undefined, }); res.setHeader("Content-Type", result.contentType); diff --git a/src/routes/status.ts b/src/routes/status.ts index b3c1fca..7b2f0a1 100644 --- a/src/routes/status.ts +++ b/src/routes/status.ts @@ -3,6 +3,21 @@ import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const router = Router(); + +/** + * @openapi + * /status: + * get: + * tags: [System] + * summary: Service status page + * operationId: statusPage + * responses: + * 200: + * description: HTML status page + * content: + * text/html: + * schema: { type: string } + */ router.get("/", (_req, res) => { res.sendFile(path.join(__dirname, "../../public/status.html")); }); diff --git a/src/services/browser.ts b/src/services/browser.ts index f416221..9e30d92 100644 --- a/src/services/browser.ts +++ b/src/services/browser.ts @@ -33,7 +33,12 @@ export function getPoolStats() { async function recyclePage(page: Page): Promise { try { - await page.goto("about:blank", { timeout: 5000 }).catch(() => {}); + // Fast reset: evaluate clears DOM without a full navigation round-trip + await page.evaluate(() => { + document.open(); + document.write(""); + document.close(); + }).catch(() => page.goto("about:blank", { timeout: 3000 }).catch(() => {})); } catch {} } @@ -115,7 +120,10 @@ async function scheduleRestart(inst: BrowserInstance): Promise { inst.browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, - args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage"], + args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage", + "--disable-background-networking", "--disable-default-apps", "--disable-extensions", + "--disable-sync", "--disable-translate", "--metrics-recording-only", + "--no-first-run", "--safebrowsing-disable-auto-update"], }); inst.availablePages.push(...await createPages(inst.browser, PAGES_PER_BROWSER)); inst.jobCount = 0; @@ -129,7 +137,10 @@ export async function initBrowser(): Promise { const browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, - args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage"], + args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage", + "--disable-background-networking", "--disable-default-apps", "--disable-extensions", + "--disable-sync", "--disable-translate", "--metrics-recording-only", + "--no-first-run", "--safebrowsing-disable-auto-update"], }); const pages = await createPages(browser, PAGES_PER_BROWSER); const staggerMs = i * (RESTART_AFTER_MS / BROWSER_COUNT); diff --git a/src/services/screenshot.ts b/src/services/screenshot.ts index 10ded50..0e67020 100644 --- a/src/services/screenshot.ts +++ b/src/services/screenshot.ts @@ -13,6 +13,7 @@ export interface ScreenshotOptions { waitForSelector?: string; deviceScale?: number; delay?: number; + waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2"; } export interface ScreenshotResult { @@ -34,6 +35,7 @@ export async function takeScreenshot(opts: ScreenshotOptions): Promise { - await page.goto(opts.url, { waitUntil: "networkidle2", timeout: 20_000 }); + await page.goto(opts.url, { waitUntil, timeout: 20_000 }); if (opts.waitForSelector) { await page.waitForSelector(opts.waitForSelector, { timeout: 10_000 });