cloudflare / cloudflare/vinext

Prod server re-runs dynamic import() on every request, expensive with ESM loader hooks

Open
#2,662 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

### Summary

The production server re-runs several dynamic `import()` calls on every request. Node caches the modules, but each `import()` still pays ESM resolution, and when ESM loader hooks are registered (`@sentry/node` or OpenTelemetry via `module.register()`) every resolution becomes a synchronous round trip to the loader hooks thread (`makeSyncRequest` in `node:internal/modules/esm/hooks`). Profiling a production app with Sentry instrumentation, this path alone accounted for about 17% of process CPU.

### Measurements

Counted with a `diagnostics_channel.tracingChannel("module.import")` subscriber inside the prod server of the `app-basic` fixture, 30 iterations over 4 routes (120 requests):

| module | imports |
|---|---|
| `config/config-matchers.js` (app-rsc-handler) | 720 (6 per request) |
| `config-headers.js` | 150 |
| `metadata-route-response` thunk (runs for every request, not only metadata routes) | 120 |
| `app-page-cache.js` (app-page-dispatch) | 60 |
| `file-based-metadata` thunk | 60 |
| `loadModule("ssr", "index")` | 60 |
| `app-route-handler-dispatch` thunk | 30 |
| `headers.js` (via `connection()` in the server shim) | 30 |
| total | about 10 per request |

### Suggested fix

Cache the import promises. Two cases seem worth distinguishing:

1. Internal framework modules (`headers.js`, `config-matchers`, `config-headers`, `app-page-cache`, the generated entry thunks, `react-dom/static.edge`): memoize unconditionally at module scope. The targets are framework code, never user code, so this is safe in dev too (when the importer is invalidated, its module scope cache resets with it).
2. `import.meta.viteRsc.loadModule("ssr" | "rsc", "index")`: the target bundles user code, so memoize only when `process.env.NODE_ENV === "production"` to keep the per call runner import that dev HMR relies on. `NODE_ENV` matches the dev detection these files already use.

With this change the same measurement drops from about 10 imports per request to 0, and the prod, dev/HMR and pages router test suites stay green.

Happy to send a PR with the change. Thanks for the great project!

Contributor guide

Open the contributing guide

Research direction

Start by locating the app-rsc-handler, app-page-dispatch, server shim, generated entry thunks, and the import.meta.viteRsc.loadModule entry point mentioned in the issue. Trace which dynamic imports run per request, then review the prod, dev/HMR, and pages router test suites. Done means repeated production imports are eliminated while dev HMR behavior remains intact and those suites stay green.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript, vite
Domain
backend, performance
Issue type
Refactor
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.