From 3d0771f2d0082b9ecabfc710def31586f50aeff8 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Thu, 15 Oct 2026 19:37:15 +0100 Subject: [PATCH] Stop per-request module re-imports leaking memory in production Page rendering imported the SSR bundle and .server.ts modules with a unique ?t= query on every request, in production too. Each one became a new, never-evicted module record (and re-ran module top-level code), so RSS grew with request count. Cache-bust only in dev via importFresh. Co-Authored-By: Claude Opus 5.5 --- .../web-pages/grab-page-bundled-react-component.js | 3 ++- .../web-pages/grab-page-combined-server-res.js | 6 +++--- .../functions/server/web-pages/grab-page-modules.js | 6 +++--- dist/utils/import-fresh.d.ts | 7 +++++++ dist/utils/import-fresh.js | 10 ++++++++++ package.json | 2 +- .../web-pages/grab-page-bundled-react-component.tsx | 3 ++- .../web-pages/grab-page-combined-server-res.ts | 13 +++++++------ .../server/web-pages/grab-page-modules.tsx | 7 +++---- src/utils/import-fresh.ts | 13 +++++++++++++ 10 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 dist/utils/import-fresh.d.ts create mode 100644 dist/utils/import-fresh.js create mode 100644 src/utils/import-fresh.ts diff --git a/dist/functions/server/web-pages/grab-page-bundled-react-component.js b/dist/functions/server/web-pages/grab-page-bundled-react-component.js index e9d2cb2..0a5b752 100644 --- a/dist/functions/server/web-pages/grab-page-bundled-react-component.js +++ b/dist/functions/server/web-pages/grab-page-bundled-react-component.js @@ -5,6 +5,7 @@ import grabRootFilePath from "./grab-root-file-path"; import path from "path"; import grabDirNames from "../../../utils/grab-dir-names"; import grabRelativePath from "../../../utils/grab-relative-path"; +import importFresh from "../../../utils/import-fresh"; const { ROOT_DIR } = grabDirNames(); export default async function grabPageBundledReactComponent({ file_path, return_tsx_only, }) { try { @@ -12,7 +13,7 @@ export default async function grabPageBundledReactComponent({ file_path, return_ if (global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[relative_path]) { const abs = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[relative_path].path); // Loader.registry.delete(abs); - const mod = await import(`${abs}?t=${Date.now()}`); + const mod = await importFresh(abs); const Main = mod.default; return { component: Main, diff --git a/dist/functions/server/web-pages/grab-page-combined-server-res.js b/dist/functions/server/web-pages/grab-page-combined-server-res.js index 4eb726b..af9564f 100644 --- a/dist/functions/server/web-pages/grab-page-combined-server-res.js +++ b/dist/functions/server/web-pages/grab-page-combined-server-res.js @@ -6,9 +6,9 @@ import grabPageServerPath from "./grab-page-server-path"; import path from "path"; import grabDirNames from "../../../utils/grab-dir-names"; import grabRelativePath from "../../../utils/grab-relative-path"; +import importFresh from "../../../utils/import-fresh"; const { ROOT_DIR } = grabDirNames(); export default async function grabPageCombinedServerRes({ file_path, debug, url, query, routeParams, }) { - const now = Date.now(); const { root_file_path } = grabRootFilePath(); const { server_file_path: root_server_file_path } = root_file_path ? grabPageServerPath({ file_path: root_file_path }) @@ -23,7 +23,7 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url, // Loader.registry.delete(final_root_server_path); } const root_server_module = final_root_server_path - ? await import(`${final_root_server_path}?t=${now}`) + ? await importFresh(final_root_server_path) : undefined; const root_server_fn = root_server_module?.default || root_server_module?.server; const rootServerRes = await grabPageServerRes({ @@ -46,7 +46,7 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url, // Loader.registry.delete(final_page_server_path); } const server_module = final_page_server_path - ? await import(`${final_page_server_path}?t=${now}`) + ? await importFresh(final_page_server_path) : undefined; const server_fn = server_module?.default || server_module?.server; const serverRes = await grabPageServerRes({ diff --git a/dist/functions/server/web-pages/grab-page-modules.js b/dist/functions/server/web-pages/grab-page-modules.js index ee626a5..c073fd2 100644 --- a/dist/functions/server/web-pages/grab-page-modules.js +++ b/dist/functions/server/web-pages/grab-page-modules.js @@ -3,8 +3,8 @@ import _ from "lodash"; import { log } from "../../../utils/log"; import grabRootFilePath from "./grab-root-file-path"; import grabPageCombinedServerRes from "./grab-page-combined-server-res"; +import importFresh from "../../../utils/import-fresh"; export default async function grabPageModules({ file_path, debug, url, query, routeParams, skip_server_res, }) { - const now = Date.now(); const { serverRes } = skip_server_res ? {} : await grabPageCombinedServerRes({ @@ -33,8 +33,8 @@ export default async function grabPageModules({ file_path, debug, url, query, ro const { root_file_path } = grabRootFilePath(); const root_module = !root_file_path ? undefined - : bundled_root_module || (await import(`${root_file_path}?t=${now}`)); - const module = page_module || (await import(`${file_path}?t=${now}`)); + : bundled_root_module || (await importFresh(root_file_path)); + const module = page_module || (await importFresh(file_path)); if (debug) { log.info(`module:`, module); } diff --git a/dist/utils/import-fresh.d.ts b/dist/utils/import-fresh.d.ts new file mode 100644 index 0000000..4020179 --- /dev/null +++ b/dist/utils/import-fresh.d.ts @@ -0,0 +1,7 @@ +/** + * Dev: bypass Bun's module registry so rebuilt files are picked up. + * Prod: import by stable path so each module is loaded (and kept) once. + * A unique `?t=` per call creates a new, never-evicted module record, so + * using it in production leaks memory on every request. + */ +export default function importFresh(file_path: string): Promise; diff --git a/dist/utils/import-fresh.js b/dist/utils/import-fresh.js new file mode 100644 index 0000000..c4d1337 --- /dev/null +++ b/dist/utils/import-fresh.js @@ -0,0 +1,10 @@ +import isDevelopment from "./is-development"; +/** + * Dev: bypass Bun's module registry so rebuilt files are picked up. + * Prod: import by stable path so each module is loaded (and kept) once. + * A unique `?t=` per call creates a new, never-evicted module record, so + * using it in production leaks memory on every request. + */ +export default function importFresh(file_path) { + return import(isDevelopment() ? `${file_path}?t=${Date.now()}` : file_path); +} diff --git a/package.json b/package.json index 9c8723b..9d3fb80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/bunext", - "version": "1.2.3", + "version": "1.2.4", "main": "dist/index.js", "module": "index.ts", "dependencies": { diff --git a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx index c1fc3b8..db5619f 100644 --- a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx +++ b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx @@ -7,6 +7,7 @@ import grabRootFilePath from "./grab-root-file-path"; import path from "path"; import grabDirNames from "../../../utils/grab-dir-names"; import grabRelativePath from "../../../utils/grab-relative-path"; +import importFresh from "../../../utils/import-fresh"; const { ROOT_DIR } = grabDirNames(); @@ -28,7 +29,7 @@ export default async function grabPageBundledReactComponent({ global.BUNEXT_SSR_BUNDLER_CTX_MAP[relative_path].path, ); // Loader.registry.delete(abs); - const mod = await import(`${abs}?t=${Date.now()}`); + const mod = await importFresh(abs); const Main = mod.default as FC; diff --git a/src/functions/server/web-pages/grab-page-combined-server-res.ts b/src/functions/server/web-pages/grab-page-combined-server-res.ts index 31afac0..fb2c722 100644 --- a/src/functions/server/web-pages/grab-page-combined-server-res.ts +++ b/src/functions/server/web-pages/grab-page-combined-server-res.ts @@ -11,6 +11,7 @@ import grabPageServerPath from "./grab-page-server-path"; import path from "path"; import grabDirNames from "../../../utils/grab-dir-names"; import grabRelativePath from "../../../utils/grab-relative-path"; +import importFresh from "../../../utils/import-fresh"; const { ROOT_DIR } = grabDirNames(); @@ -29,8 +30,6 @@ export default async function grabPageCombinedServerRes({ query, routeParams, }: Params) { - const now = Date.now(); - const { root_file_path } = grabRootFilePath(); const { server_file_path: root_server_file_path } = root_file_path ? grabPageServerPath({ file_path: root_file_path }) @@ -48,8 +47,9 @@ export default async function grabPageCombinedServerRes({ if (final_root_server_path) { // Loader.registry.delete(final_root_server_path); } - const root_server_module: BunextPageServerModule = final_root_server_path - ? await import(`${final_root_server_path}?t=${now}`) + const root_server_module: BunextPageServerModule | undefined = + final_root_server_path + ? await importFresh(final_root_server_path) : undefined; const root_server_fn = @@ -78,8 +78,9 @@ export default async function grabPageCombinedServerRes({ if (final_page_server_path) { // Loader.registry.delete(final_page_server_path); } - const server_module: BunextPageServerModule = final_page_server_path - ? await import(`${final_page_server_path}?t=${now}`) + const server_module: BunextPageServerModule | undefined = + final_page_server_path + ? await importFresh(final_page_server_path) : undefined; const server_fn = server_module?.default || server_module?.server; diff --git a/src/functions/server/web-pages/grab-page-modules.tsx b/src/functions/server/web-pages/grab-page-modules.tsx index fe1e8f5..0a24574 100644 --- a/src/functions/server/web-pages/grab-page-modules.tsx +++ b/src/functions/server/web-pages/grab-page-modules.tsx @@ -10,6 +10,7 @@ import { log } from "../../../utils/log"; import grabRootFilePath from "./grab-root-file-path"; import grabPageCombinedServerRes from "./grab-page-combined-server-res"; import type { FC } from "react"; +import importFresh from "../../../utils/import-fresh"; type Params = { file_path: string; @@ -35,8 +36,6 @@ export default async function grabPageModules({ routeParams, skip_server_res, }: Params): Promise { - const now = Date.now(); - const { serverRes } = skip_server_res ? {} : await grabPageCombinedServerRes({ @@ -77,10 +76,10 @@ export default async function grabPageModules({ const { root_file_path } = grabRootFilePath(); const root_module: BunextRootModule | undefined = !root_file_path ? undefined - : bundled_root_module || (await import(`${root_file_path}?t=${now}`)); + : bundled_root_module || (await importFresh(root_file_path)); const module: BunextPageModule = - page_module || (await import(`${file_path}?t=${now}`)); + page_module || (await importFresh(file_path)); if (debug) { log.info(`module:`, module); diff --git a/src/utils/import-fresh.ts b/src/utils/import-fresh.ts new file mode 100644 index 0000000..018ea0c --- /dev/null +++ b/src/utils/import-fresh.ts @@ -0,0 +1,13 @@ +import isDevelopment from "./is-development"; + +/** + * Dev: bypass Bun's module registry so rebuilt files are picked up. + * Prod: import by stable path so each module is loaded (and kept) once. + * A unique `?t=` per call creates a new, never-evicted module record, so + * using it in production leaks memory on every request. + */ +export default function importFresh(file_path: string): Promise { + return import( + isDevelopment() ? `${file_path}?t=${Date.now()}` : file_path + ); +}