Skip to content

perf(artifacts): precompile artifacts at save time so the frame skips Sucrase and Tailwind - #28

Merged
fforres merged 3 commits into
perf/shell-drop-tailwind-jitfrom
perf/artifact-precompile
Oct 18, 2026
Merged

fforres merged 3 commits into
perf/shell-drop-tailwind-jitfrom
perf/artifact-precompile

Conversation

@fforres

@fforres fforres commented Oct 18, 2026

Copy link
Copy Markdown

Stacked on #27.

Why

Every artifact view compiled the stored JSX with Sucrase, and JIT-compiled its Tailwind classes, inside the sandboxed frame. The srcdoc also carried both compilers: the 271 KB Tailwind browser bundle and about 200 KB of Sucrase.

What changes

Three commits:

  1. Compiler (mcp-apps-shell): a React-free precompileArtifact that runs Sucrase (same options as the frame) and then the Tailwind v4 compile() API (same input as the frame).
    • A bracket-aware candidate extractor feeds Tailwind.
    • Classes built with interpolation are detected and get css: null.
    • Code above 256 KiB is not precompiled.
    • A generated compile fingerprint has a pin test.
    • tailwindcss and @tailwindcss/browser are pinned to one version (4.2.2).
  2. Storage and serving (sdk/api/host-cloudflare/cloud): three nullable columns, compiled_fingerprint, compiled_js and compiled_css.
    • Every save compiles. It is fail-open: a compile failure stores NULLs and never refuses the save.
    • New getForRender recompiles stale or missing output and writes it back. The write is guarded on the compiled code, so a concurrent save always wins.
    • get and every tool-call path skip the compiled columns.
    • GET /api/artifacts/:id returns compiled. Clients cannot send it.
    • Only host-cloudflare wires the compiler, through a lazy import.
    • The cloud migration 0022 is generated. D1 adds the columns at runtime.
  3. Frame (shell/react): the shell checks the fingerprint, then builds the srcdoc per half. CSS comes from storage or from runtime Tailwind; JS comes from storage or from a separately loaded compiler script. Each half falls back on its own. Sucrase is no longer in the inner renderer bundle. The artifact-style skill asks for complete literal class names.

Measurements

  • Fast-path srcdoc: about 1.25 MB, down from about 1.73 MB (production estimate).
  • In-frame compiles: none on the fast path (asserted by test).
  • Server compile: about 3 to 7 ms CPU per artifact (12 to 45 KB source).

Tests

  • Unit: extractor, precompile, fingerprint pin, and a Worker-compat graph check.
  • sdk: compile on save, getForRender backfill, and the concurrent-save race.
  • api: compiled is returned, and a client-sent compiled is not stored.
  • D1: columns are added to an existing database.
  • Browser: fast path, stored JS used, fingerprint-mismatch fallback, dynamic classes keep Tailwind, and the corpus guard.
  • e2e: cloudflare/artifact-precompiled-styles.test.ts passes in workerd.
  • Typecheck: 44 of 45 tasks pass. The failure is @executor-js/desktop (electron.vite.config.ts plugin type mismatch), a known upstream issue not touched here.

https://claude.ai/code/session_019ZE3j9YzRKxT4LaiDCBv6S

Add a React-free precompileArtifact() that runs the Sucrase JSX transform and a
Tailwind build against the same input the sandboxed frame compiles at runtime.
Class candidates come from a bracket-aware source scanner, and code that builds
class names dynamically is detected so it keeps the runtime path. A generated
fingerprint over the compiler, extractor, theme and pinned versions marks
compiled output as current. Nothing is wired yet.

Pin tailwindcss and @tailwindcss/browser to one catalog version so the
precompiled CSS and the runtime fallback agree.

Claude-Session: https://claude.ai/code/session_019ZE3j9YzRKxT4LaiDCBv6S
…nder reads

Add an optional ArtifactCompiler port to the executor config. When a host
provides one, artifacts.save compiles the source and stores the fingerprint,
JS and CSS in three new nullable columns. The compile step is fail-open: a
failure saves the artifact with empty compiled columns. artifacts.get stays
light and does not read the compiled columns. The new artifacts.getForRender
returns them, and recompiles and writes back output stored under an older
fingerprint, guarded on the source it compiled so a concurrent save wins.

The HTTP get endpoint now returns an optional compiled field. The save payload
is unchanged, so clients cannot supply compiled output. host-cloudflare wires
the compiler through a lazy import. Includes the cloud schema and migration
regeneration for the new columns.

Claude-Session: https://claude.ai/code/session_019ZE3j9YzRKxT4LaiDCBv6S
…in the frame

When an artifact arrives with output from the server compiler that matches the
shell's own fingerprint, the renderer frame embeds the stored CSS and runs the
stored JS. It loads neither the Tailwind browser compiler nor Sucrase, so the
srcdoc is smaller and the first paint is already styled. Any mismatch, or a
missing half (classes built at runtime keep Tailwind, uncompilable code keeps
Sucrase), falls back to the previous behavior per half.

Sucrase moves out of the inner renderer into its own bundle served as
virtual:executor-inner-compiler and only included when needed. The console
passes the stored compiled output to the shell on both the tool input and the
tool result. The artifact-style skill now tells the model to write every class
as a complete literal.

Claude-Session: https://claude.ai/code/session_019ZE3j9YzRKxT4LaiDCBv6S
@fforres
fforres merged commit a01b5ab into perf/shell-drop-tailwind-jit Oct 18, 2026
1 of 37 checks passed
@fforres
fforres deleted the perf/artifact-precompile branch October 18, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant