next

Browser Automation in Next.js Serverless Functions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "next" with this command: npx skills add vercel-labs/agent-browser/vercel-labs-agent-browser-next

Browser Automation in Next.js Serverless Functions

Run headless Chrome directly inside Next.js server actions and API routes using @sparticuz/chromium

  • puppeteer-core . No external server needed -- Chrome runs in the same serverless function.

Dependencies

pnpm add @sparticuz/chromium puppeteer-core

Core Pattern

import puppeteer from "puppeteer-core"; import chromium from "@sparticuz/chromium"; import fs from "node:fs";

const CHROME_PATHS = [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", ];

function findLocalChrome(): string { for (const p of CHROME_PATHS) { if (fs.existsSync(p)) return p; } throw new Error( Chrome not found. Set CHROMIUM_PATH to your Chrome/Chromium binary., ); }

async function launchBrowser() { const isLambda = !!process.env.VERCEL || !!process.env.AWS_LAMBDA_FUNCTION_NAME;

const executablePath = isLambda ? await chromium.executablePath() : process.env.CHROMIUM_PATH || findLocalChrome();

const args = isLambda ? chromium.args : ["--no-sandbox", "--disable-setuid-sandbox"];

return puppeteer.launch({ args, executablePath, headless: true, defaultViewport: { width: 1280, height: 720 }, }); }

On Vercel, @sparticuz/chromium bundles a compatible Chromium binary automatically. Locally, the launcher falls back to the system Chrome installation or CHROMIUM_PATH .

Server Actions

Screenshot

"use server";

export async function takeScreenshot(url: string) { const browser = await launchBrowser(); try { const page = await browser.newPage(); await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 }); const title = await page.title(); const screenshot = await page.screenshot({ fullPage: true, encoding: "base64", }); return { ok: true, title, screenshot: screenshot as string }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } finally { await browser.close(); } }

Accessibility Snapshot

"use server";

export async function takeSnapshot(url: string) { const browser = await launchBrowser(); try { const page = await browser.newPage(); await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 }); const title = await page.title(); const snapshot = await page.accessibility.snapshot(); return { ok: true, title, snapshot: JSON.stringify(snapshot, null, 2) }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } finally { await browser.close(); } }

API Routes

// app/api/browse/route.ts import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) { const { url, action } = await req.json();

if (!url) { return NextResponse.json({ error: "Provide a 'url'" }, { status: 400 }); }

const browser = await launchBrowser(); try { const page = await browser.newPage(); await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 });

if (action === "screenshot") {
  const screenshot = await page.screenshot({ encoding: "base64" });
  return NextResponse.json({ screenshot });
}

if (action === "snapshot") {
  const snapshot = await page.accessibility.snapshot();
  return NextResponse.json({ snapshot });
}

return NextResponse.json(
  { error: "action must be 'screenshot' or 'snapshot'" },
  { status: 400 },
);

} finally { await browser.close(); } }

Environment Variables

Variable Required Description

CHROMIUM_PATH

Local dev only Path to Chrome/Chromium binary. Not needed on Vercel.

On Vercel, @sparticuz/chromium auto-detects the bundled binary. Locally, if Chrome is not in a standard location, set CHROMIUM_PATH .

Vercel Configuration

The @sparticuz/chromium binary is large (~50MB). Increase the serverless function's memory and timeout if needed:

// next.config.ts const nextConfig = { serverExternalPackages: ["@sparticuz/chromium"], }; export default nextConfig;

If the project lives in a monorepo subdirectory, set outputFileTracingRoot so the Chromium binary is included in the deployment:

import path from "node:path";

const nextConfig = { outputFileTracingRoot: path.join(import.meta.dirname, "../../"), serverExternalPackages: ["@sparticuz/chromium"], }; export default nextConfig;

Limitations

  • Vercel serverless functions have a 50MB compressed size limit. @sparticuz/chromium fits within this but leaves limited room for other large dependencies.

  • Function execution timeout is 10s on Hobby, 300s on Pro. Complex page loads may need the Pro plan.

  • Each invocation launches a fresh browser. There is no session persistence between requests.

  • For workflows that need persistent sessions, longer timeouts, or full Chrome (no size limits), use the Vercel Sandbox pattern instead (see the vercel-sandbox skill).

Example

See examples/demo/ in the agent-browser repo for a working app with both serverless and sandbox patterns, and a deploy-to-Vercel button.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

dogfood

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

electron

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

skill-creator

No summary provided by upstream source.

Repository SourceNeeds Review