From 1de51a9afdb1d972854691ff6fe26e6424092d28 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 10 Aug 2026 08:43:45 +0200 Subject: [PATCH] docs(findings): record the DB-driver chop as measured, attempted, and backed out mysql2 (77 files) and pg (13 files) load on every SQLite install because db/drivers/index.ts imports all three wrappers statically. Re-measured with a forced GC between readings, the prize is 21.4 MiB RSS - not the ~30 MiB the naive sum implied, and drizzle-orm's 124.3 MiB in the require-cost sweep was almost entirely that same no-GC artefact. Three attempts, each caught by booting the real image rather than by typecheck: createRequire(import.meta.url) is undefined in the CommonJS migrate.cjs bundle and broke startup for every dialect; a dynamic require resolves to a .cjs build Next never traces; and leaving drizzle's dialect static defeats the purpose because drizzle-orm/mysql2 itself imports mysql2. The two remaining routes are worse than the problem - a second copy of drizzle's core alongside the ESM one, or hardcoding drizzle's internal file layout so an upgrade breaks startup for MySQL and Postgres. Backed out; the structural blocker is that createDb must stay synchronous. --- PHASE2-FINDINGS.md | 47 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/PHASE2-FINDINGS.md b/PHASE2-FINDINGS.md index cd50be803..c846488ea 100644 --- a/PHASE2-FINDINGS.md +++ b/PHASE2-FINDINGS.md @@ -714,12 +714,47 @@ against whether the server actually loads them: These are not additive — dockerode's figure contains ssh2's — but the ranking is what matters. -**mysql2 and pg are loaded on every SQLite install.** `db/drivers/index.ts` statically imports -all three driver wrappers and picks one at call time, so importing the module executes all -three. Not yet fixed: `packages/db/index.ts` does `export const db = createDb(schema)` at -module scope, so `createDb` must stay synchronous and cannot use `await import()`. The viable -route is deferring the external `require` inside each wrapper. Both dialects have testcontainer -migration tests, so it is verifiable — it is queued, not dismissed. +> **Correction.** The RSS figures in the table above were measured without forcing a GC +> between readings, so they include V8 growing its heap opportunistically rather than the real +> cost of the module. Re-measured with `global.gc()` before each reading, the two unused +> database drivers cost **21.4 MiB RSS / 4.1 MiB heap**, not the ~30 MiB the naive sum implied +> — and `drizzle-orm`'s 124.3 MiB was almost entirely this artefact. The ssh2 conclusion is +> unaffected: it was confirmed independently by the before/after external-memory drop. + +### mysql2 and pg load on every SQLite install — and it was not worth fixing + +`db/drivers/index.ts` statically imports all three driver wrappers and picks one at call time, +so importing the module executes all three. Confirmed at runtime: mysql2 (77 files) and pg +(13 files) are resident on a SQLite install. Re-measured properly, the prize is **21.4 MiB**. + +Three attempts, each caught by testing the real image rather than by typecheck: + +1. `createRequire(import.meta.url)` — this module is bundled into two entry points, and + `db/migrations//migrate.cjs` is **CommonJS**, where `import.meta.url` compiles to + `undefined`. `createRequire` threw `ERR_INVALID_ARG_VALUE` at load and took down migrations, + and therefore startup, **for every dialect including SQLite**. Fixed by basing resolution on + `process.cwd()`. +2. With that fixed, MySQL failed with `MODULE_NOT_FOUND` on + `drizzle-orm/mysql2/index.cjs`. A dynamic `require` resolves through the package's `require` + condition to a `.cjs` build, and Next's tracing only ever copied the ESM `index.js` into the + standalone output. +3. Leaving drizzle's dialect as a static import defeats the purpose entirely: + `drizzle-orm/mysql2` itself does `from "mysql2"`, so the client loads anyway. Verified — the + built image still loaded all 77 mysql2 files. + +That leaves two routes, and both are worse than the problem. Forcing drizzle's `.cjs` into the +image via `outputFileTracingIncludes` would load **a second copy of drizzle's core** next to +the ESM one for MySQL users, so class identities and `is()` checks would straddle two module +instances. Requiring the ESM file works under Node 24's `require(esm)` — but only via an +absolute path (`/app/node_modules/drizzle-orm/mysql2/index.js`); both bare specifiers fail, +so it would hardcode drizzle's internal file layout and a drizzle upgrade would break +*startup* for MySQL and Postgres users. + +**Backed out.** 21.4 MiB is not worth a startup-failure class of risk on two dialects that +cannot be exercised in normal development. The blocker to record is structural: `createDb` +must stay synchronous because `packages/db/index.ts` does `export const db = createDb(schema)` +at module scope and `migrate.cjs` is CommonJS, so neither `await import()` nor top-level await +is available. Making the database handle async would unblock it. ## Why 24 MiB of code is resident before the first request