From 2bc15628c50c4aa09320f2a743ed6d4d235ec2ec Mon Sep 17 00:00:00 2001 From: RECTOR Date: Mon, 7 Sep 2026 01:29:44 +0700 Subject: [PATCH] =?UTF-8?q?fix(render):=20lazy=20playwright-core=20?= =?UTF-8?q?=E2=80=94=20static=20import=20cost=20~20s=20per=20omp=20startup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit omp's guarded extension loader preloads every statically resolvable import of an extension module at host startup — including import() literals — and pays ~20s for playwright-core's ~9MB graph (native bun import: 0.57s). chromium is only needed when a render executes. The specifier is assembled at runtime (['playwright','-core'].join()) so no static analysis can pull it into the preload set; it loads once, memoized, on first actual render. Verified under bun: full render (screenshot + computed styles + dom snapshot) works with the indirected import; omp startup 27.7s -> 5.2s. Fixes #21 --- extensions/render.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/extensions/render.ts b/extensions/render.ts index 3f0c786..ad1cb42 100644 --- a/extensions/render.ts +++ b/extensions/render.ts @@ -1,11 +1,31 @@ // extensions/render.ts -import { chromium } from "playwright-core" +// NOTE: playwright-core must not be referenced in ANY static import — not +// even `import type`. omp's guarded extension loader preloads every import +// specifier it sees at load time; the ~9 MB graph cost ~20 s per startup (#21). +// Types flow via inference from the dynamic import below. import { Type } from "typebox" import { pathToFileURL } from "node:url" import { writeFileSync, mkdirSync } from "node:fs" import { join } from "node:path" import { toOklchString } from "../engine/color.mjs" +// Memoized lazy loader. The `ReturnType`-derived annotation is deliberate: +// naming a concrete type would require a static playwright-core import, +// which omp's loader forbids here (#21) — this stays module-private. +let cachedChromium: Awaited> | undefined +async function loadChromium() { + // Indirected specifier: omp's guarded loader preloads every statically + // resolvable import — including dynamic import() literals — at startup + // (#21). Assembling the name at runtime keeps the ~9 MB graph out of the + // preload set; it loads once, on first actual render. + const spec = ["playwright", "-core"].join("") + const mod = await import(spec) + return mod.chromium +} +async function getChromium() { + cachedChromium ??= await loadChromium() + return cachedChromium +} interface RenderInput { htmlPath: string @@ -42,7 +62,7 @@ export async function render(input: RenderInput): Promise { const viewports = input.viewports ?? [1280, 375, 320, 414, 768] const outDir = input.outDir ?? "./keystone-render" mkdirSync(outDir, { recursive: true }) - const browser = await chromium.launch({ headless: true }) + const browser = await (await getChromium()).launch({ headless: true }) const screenshots: { width: number; path: string }[] = [] const computedPairs: { selector: string; color: string; backgroundColor: string; width: number; height: number }[] = [] const viewportMetrics: ViewportMetric[] = []