cloudflare / cloudflare/vinext
Prod server re-runs dynamic import() on every request, expensive with ESM loader hooks
- 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
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