Skip to content

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

Description

@jlucaso1

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions