perf: switch to domcontentloaded default, optimize browser pool, fix swagger paths
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 7m59s

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
This commit is contained in:
OpenClaw 2026-02-20 12:39:06 +00:00
parent de1215bc32
commit d20fbbfe2e
5 changed files with 43 additions and 6 deletions

View file

@ -47,7 +47,7 @@ const options: swaggerJsdoc.Options = {
},
},
},
apis: ["./src/routes/*.ts"],
apis: ["./src/routes/*.ts", "./dist/routes/*.js"],
};
export const openapiSpec = swaggerJsdoc(options);

View file

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

View file

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

View file

@ -33,7 +33,12 @@ export function getPoolStats() {
async function recyclePage(page: Page): Promise<void> {
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("<html><head></head><body></body></html>");
document.close();
}).catch(() => page.goto("about:blank", { timeout: 3000 }).catch(() => {}));
} catch {}
}
@ -115,7 +120,10 @@ async function scheduleRestart(inst: BrowserInstance): Promise<void> {
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<void> {
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);

View file

@ -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<Screensho
const fullPage = opts.fullPage ?? false;
const quality = format === "png" ? undefined : Math.min(Math.max(opts.quality || 80, 1), 100);
const deviceScale = Math.min(opts.deviceScale || 1, 3);
const waitUntil = opts.waitUntil || "domcontentloaded";
const { page, instance } = await acquirePage();
@ -42,7 +44,7 @@ export async function takeScreenshot(opts: ScreenshotOptions): Promise<Screensho
await Promise.race([
(async () => {
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 });