next
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBrowser Automation in Next.js Serverless Functions
在Next.js无服务器函数中实现浏览器自动化
Run headless Chrome directly inside Next.js server actions and API routes using + . No external server needed -- Chrome runs in the same serverless function.
@sparticuz/chromiumpuppeteer-core使用 + 直接在Next.js Server Actions和API路由中运行无头Chrome。无需外部服务器——Chrome直接在同一无服务器函数中运行。
@sparticuz/chromiumpuppeteer-coreDependencies
依赖
bash
pnpm add @sparticuz/chromium puppeteer-corebash
pnpm add @sparticuz/chromium puppeteer-coreCore Pattern
核心模式
ts
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, bundles a compatible Chromium binary automatically. Locally, the launcher falls back to the system Chrome installation or .
@sparticuz/chromiumCHROMIUM_PATHts
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 },
});
}在Vercel上,会自动打包兼容的Chromium二进制文件。在本地环境中,启动器会回退到系统Chrome安装路径或配置的路径。
@sparticuz/chromiumCHROMIUM_PATHServer Actions
Server Actions
Screenshot
截图
ts
"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();
}
}ts
"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
无障碍快照
ts
"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();
}
}ts
"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
API路由
ts
// 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();
}
}ts
// 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 |
|---|---|---|
| Local dev only | Path to Chrome/Chromium binary. Not needed on Vercel. |
On Vercel, auto-detects the bundled binary. Locally, if Chrome is not in a standard location, set .
@sparticuz/chromiumCHROMIUM_PATH| 变量名 | 是否必填 | 描述 |
|---|---|---|
| 仅本地开发需要 | Chrome/Chromium二进制文件的路径。在Vercel上无需配置。 |
在Vercel上,会自动检测打包的二进制文件。在本地环境中,如果Chrome不在标准路径下,需设置。
@sparticuz/chromiumCHROMIUM_PATHVercel Configuration
Vercel配置
The binary is large (~50MB). Increase the serverless function's memory and timeout if needed:
@sparticuz/chromiumts
// next.config.ts
const nextConfig = {
serverExternalPackages: ["@sparticuz/chromium"],
};
export default nextConfig;If the project lives in a monorepo subdirectory, set so the Chromium binary is included in the deployment:
outputFileTracingRootts
import path from "node:path";
const nextConfig = {
outputFileTracingRoot: path.join(import.meta.dirname, "../../"),
serverExternalPackages: ["@sparticuz/chromium"],
};
export default nextConfig;@sparticuz/chromiumts
// next.config.ts
const nextConfig = {
serverExternalPackages: ["@sparticuz/chromium"],
};
export default nextConfig;如果项目位于 monorepo 的子目录中,需设置以确保Chromium二进制文件被包含在部署包中:
outputFileTracingRootts
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. fits within this but leaves limited room for other large dependencies.
@sparticuz/chromium - 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 skill).
vercel-sandbox
- Vercel无服务器函数的压缩包大小限制为50MB。符合该限制,但留给其他大型依赖的空间有限。
@sparticuz/chromium - Hobby计划的函数执行超时时间为10秒,Pro计划为300秒。复杂页面加载可能需要Pro计划。
- 每次调用都会启动一个全新的浏览器,请求之间没有会话持久化。
- 对于需要持久会话、更长超时时间或完整Chrome(无大小限制)的工作流,建议使用Vercel Sandbox模式(详见技能)。
vercel-sandbox
Example
示例
See in the agent-browser repo for a working app with both serverless and sandbox patterns, and a deploy-to-Vercel button.
examples/demo/可查看agent-browser仓库中的目录,获取同时包含无服务器和Sandbox模式的可运行应用,以及一键部署到Vercel的按钮。
examples/demo/