diff --git a/scripts/dev-benchmark.ts b/scripts/dev-benchmark.ts new file mode 100644 index 0000000..41b33f4 --- /dev/null +++ b/scripts/dev-benchmark.ts @@ -0,0 +1,132 @@ +const viteArgs = process.argv.slice(2); +const host = "127.0.0.1"; +const port = 3020; +const start = performance.now(); + +const child = Bun.spawn({ + cmd: ["bun", "run", "dev", "--", "--host", host, "--port", String(port), ...viteArgs], + cwd: process.cwd(), + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + env: process.env, +}); + +let ready = false; +let stdout = ""; +let stderr = ""; + +async function collect(readable: ReadableStream, target: "stdout" | "stderr") { + const decoder = new TextDecoder(); + + for await (const chunk of readable) { + const text = decoder.decode(chunk, { stream: true }); + + if (target === "stdout") { + stdout += text; + if (stdout.includes("Local:")) { + ready = true; + } + } else { + stderr += text; + } + } + + const tail = decoder.decode(); + if (!tail) return; + + if (target === "stdout") { + stdout += tail; + if (stdout.includes("Local:")) { + ready = true; + } + } else { + stderr += tail; + } +} + +const stdoutTask = collect(child.stdout, "stdout"); +const stderrTask = collect(child.stderr, "stderr"); + +async function waitForReady(timeoutMs: number) { + const start = performance.now(); + + while (!ready) { + if (child.exitCode !== null) { + throw new Error(`vite dev exited early with code ${child.exitCode}`); + } + if (performance.now() - start > timeoutMs) { + throw new Error("timed out waiting for vite dev"); + } + await Bun.sleep(100); + } +} + +async function request(pathname: string) { + const requestStart = performance.now(); + const response = await fetch(`http://${host}:${port}${pathname}`); + await response.arrayBuffer(); + + return { + pathname, + status: response.status, + ms: Number((performance.now() - requestStart).toFixed(2)), + }; +} + +function formatDuration(ms: number) { + return `${(ms / 1000).toFixed(2)}s`; +} + +function printTable( + rows: Array<{ + step: string; + durationMs: number; + status: string; + }>, +) { + const headers = ["Step", "Duration", "Status"]; + const body = rows.map((row) => [row.step, formatDuration(row.durationMs), row.status]); + const widths = headers.map((header, index) => + Math.max(header.length, ...body.map((row) => row[index].length)), + ); + + const formatRow = (row: string[]) => + row.map((cell, index) => cell.padEnd(widths[index])).join(" "); + + console.log(formatRow(headers)); + console.log(widths.map((width) => "-".repeat(width)).join(" ")); + for (const row of body) { + console.log(formatRow(row)); + } +} + +try { + await waitForReady(30_000); + const readyMs = Number((performance.now() - start).toFixed(2)); + + const docs = await request("/docs/"); + const totalToDocsMs = Number((performance.now() - start).toFixed(2)); + const anotherPage = await request("/docs/support/dashboard"); + const totalMs = Number((performance.now() - start).toFixed(2)); + + if (docs.status >= 400 || anotherPage.status >= 400) { + console.error("vite dev benchmark hit an error response"); + if (stdout) console.error(stdout.trim()); + if (stderr) console.error(stderr.trim()); + process.exitCode = 1; + } + + printTable([ + { step: "Dev ready", durationMs: readyMs, status: "-" }, + { step: "First /docs/ request", durationMs: docs.ms, status: String(docs.status) }, + { step: "Start -> first /docs/", durationMs: totalToDocsMs, status: String(docs.status) }, + { step: "Second docs page", durationMs: anotherPage.ms, status: String(anotherPage.status) }, + { step: "Full benchmark", durationMs: totalMs, status: "-" }, + ]); +} finally { + child.kill("SIGTERM"); + await child.exited; + await stdoutTask; + await stderrTask; +} diff --git a/source.config.ts b/source.config.ts index 09371bf..bd2e582 100644 --- a/source.config.ts +++ b/source.config.ts @@ -16,13 +16,17 @@ export const docs = defineDocs({ docs: { async: isDevelopment, postprocess: { - includeProcessedMarkdown: true, + includeProcessedMarkdown: !isDevelopment, }, }, }); export default defineConfig({ mdxOptions: { + // Shiki highlighting is one of the most expensive parts of the MDX pipeline. + // Keep it in builds, but skip it in local dev so first-page SSR doesn't have + // to highlight hundreds of code-heavy docs up front. + rehypeCodeOptions: isDevelopment ? false : undefined, remarkPlugins: (existing) => [ remarkImagePaths, remarkLinkPaths, diff --git a/src/lib/source.ts b/src/lib/source.ts index 7a0326c..1edc49c 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -135,7 +135,10 @@ export const source = loader({ }); export async function getLLMText(page: InferPageType) { - const processed = await page.data.getText("processed"); + // Dev disables processed markdown generation to avoid paying that extra MDX + // postprocess cost on the first docs request, so fall back to raw text there. + const textType = process.env.NODE_ENV === "development" ? "raw" : "processed"; + const processed = await page.data.getText(textType); return `# ${page.data.title} diff --git a/src/routes/api/search.ts b/src/routes/api/search.ts index f05e315..e6167c4 100644 --- a/src/routes/api/search.ts +++ b/src/routes/api/search.ts @@ -2,7 +2,19 @@ import { createFileRoute } from "@tanstack/react-router"; import { source } from "@/lib/source"; import { createFromSource } from "fumadocs-core/search/server"; -const server = createFromSource(source, { language: "english" }); +let server: + | ReturnType + | null = null; + +function getServer() { + if (server) return server; + + // Building the local Orama index is expensive; keep it off the main dev + // startup path and only initialize it when /api/search is actually hit. + server = createFromSource(source, { language: "english" }); + + return server; +} export const Route = createFileRoute("/api/search")({ server: { @@ -13,5 +25,5 @@ export const Route = createFileRoute("/api/search")({ }); export function handleSearchGet(request: Request) { - return server.GET(request); + return getServer().GET(request); } diff --git a/src/routes/llms[.]mdx.docs.$.ts b/src/routes/llms[.]mdx.docs.$.ts index 5585807..b947d14 100644 --- a/src/routes/llms[.]mdx.docs.$.ts +++ b/src/routes/llms[.]mdx.docs.$.ts @@ -8,8 +8,11 @@ export const Route = createFileRoute("/llms.mdx/docs/$")({ const slugs = params._splat?.split("/") ?? []; const page = source.getPage(slugs); if (!page) throw notFound(); + // Dev disables processed markdown generation to keep the first docs + // request fast, so the LLM endpoint serves raw markdown in development. + const textType = process.env.NODE_ENV === "development" ? "raw" : "processed"; - return new Response(await page.data.getText("processed"), { + return new Response(await page.data.getText(textType), { headers: { "Content-Type": "text/markdown", "Access-Control-Allow-Origin": "*",