Skip to content

perf(plugins): skip dynamic-request AST parse for static-import-only modules — 5000-route build −21%#2392

Merged
james-elicx merged 1 commit into
cloudflare:mainfrom
hyf0:perf/skip-static-import-parse
Jun 28, 2026
Merged

perf(plugins): skip dynamic-request AST parse for static-import-only modules — 5000-route build −21%#2392
james-elicx merged 1 commit into
cloudflare:mainfrom
hyf0:perf/skip-static-import-parse

Conversation

@hyf0

@hyf0 hyf0 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

No dependencies — reviewable and mergeable on its own. Touches only plugins/ (ast-utils + ignore-dynamic-requests + extensionless-dynamic-import).

What

Extracts a shared, documented pre-parse gate — DYNAMIC_IMPORT_PRESCAN / mayContainDynamicImport in ast-utils — and uses it as the "might this module contain a dynamic import(...)?" check in:

  • ignore-dynamic-requests (in-handler prescan), and
  • extensionless-dynamic-import (Rolldown transform.filter.code, which already used the same idea inline).

Why

ignore-dynamic-requests pre-filtered with:

if (!code.includes("require") && !code.includes("import")) return null;

The import substring matches almost every ESM module (every static import x from "…"), so the plugin ran parseAst + a full AST walk on essentially the entire module graph — then found nothing to do in the vast majority. It was the only one of vinext's AST-walking transform plugins without a tight code filter; the siblings already gate natively (require-context/\brequire\b…\.context/, extensionless-dynamic-importimport(, typeof-windowtypeof window, import-meta-urlimport.meta…).

Static imports never put a ( (or a comment leading to one) right after the import keyword, so narrowing the import side to dynamic-call syntax skips static-import-only modules entirely. The require side stays a broad substring check (it must keep covering aliasing and comment-separated require/* … */().

Pulling the pattern into a named, documented helper makes the intent explicit — this is a deliberate, measured performance gate, not an incidental regex — and lets both plugins share one source of truth. extensionless-dynamic-import additionally gains correct handling of comment-separated dynamic imports (import/* … */("…")), which its old \bimport\s*\( filter missed.

Measured

On the 5000-route stress fixture from #2388 (same machine, clean builds, median of 3):

Build (5000 routes)
before ~22.2 s
after ~17.5 s
−21%

CPU profile of the build: jsonParseAst 1536 → 510 ms, forEachAstChild 1199 ms → off the chart, with matching GC relief — i.e. ~5000 redundant module parses removed. The saving is in AST parsing and is orthogonal to #2389 (the route-graph read cache); they stack.

Correctness

  • The gate is intentionally over-inclusive: a false positive costs one redundant parse, a false negative would silently skip a real dynamic import — so \s*[(/] tolerates whitespace and block/line comments between import and (, and require stays broad.
  • Dynamic-request / dynamic-import suites pass — dynamic-requests-build, extensionless-dynamic-import, import-meta-url: 100 tests. vp lint clean.
  • Equivalence is asserted via these suites and unchanged transform behavior, not output bytes: vinext's production build is not bit-reproducible run-to-run.

@hyf0 hyf0 marked this pull request as ready for review June 28, 2026 07:59
@hyf0 hyf0 force-pushed the perf/skip-static-import-parse branch from 9640731 to c0161e7 Compare June 28, 2026 08:04
@github-actions

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared d9bd689 against base da086c0 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 122.0 KB 122.0 KB ⚫ -0.0%
Client entry size (gzip) vinext 117.0 KB 117.0 KB ⚫ -0.0%
Dev server cold start vinext 2.34 s 2.36 s ⚫ +0.6%
Production build time vinext 2.77 s 2.73 s ⚫ -1.2%
RSC entry closure size (gzip) vinext 94.3 KB 94.3 KB ⚫ +0.0%
Server bundle size (gzip) vinext 160.1 KB 160.0 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx james-elicx merged commit 91c9c18 into cloudflare:main Jun 28, 2026
46 of 47 checks passed
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.

2 participants