From 329b4ec4e2f84e590f027511877ec9b5ed3d0ca7 Mon Sep 17 00:00:00 2001 From: Anso Date: Tue, 28 Apr 2026 01:37:39 -0400 Subject: [PATCH] perf(backend): lazy-load composerize and isomorphic-git (#819) Both modules are opt-in: - composerize (~2 MB) is only used by /api/convert when a user pastes a docker run command into the converter UI. - isomorphic-git plus isomorphic-git/http/node (~5 MB combined) only fire when a stack is created from a Git source. Previously each was imported at module scope, parsing the whole package on every cold start regardless of whether the feature was used. Wrap them in small load-and-cache helpers so the first call resolves the module via Node's loader and every subsequent call returns the cached reference. The pattern matches the existing dynamic import of @aws-sdk/client-ecr in RegistryService. Existing tests using vi.mock('isomorphic-git', ...) and vi.mock('isomorphic-git/http/node', ...) keep working without changes because dynamic and static imports share the same module registry. --- backend/src/routes/convert.ts | 13 +++++++++++-- backend/src/services/GitSourceService.ts | 24 ++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/convert.ts b/backend/src/routes/convert.ts index 875c456f..6a7c6313 100644 --- a/backend/src/routes/convert.ts +++ b/backend/src/routes/convert.ts @@ -1,11 +1,19 @@ import { Router, type Request, type Response } from 'express'; -// @ts-ignore - composerize lacks proper type definitions -import composerize from 'composerize'; import { authMiddleware } from '../middleware/auth'; import { sanitizeForLog } from '../utils/safeLog'; const MAX_DOCKER_RUN_LENGTH = 8192; +// composerize is only used when a user pastes a `docker run` command into the +// converter UI. Lazy-load it so cold boot does not parse the ~2 MB module. +let cachedComposerize: ((dockerRun: string) => string) | undefined; +async function loadComposerize(): Promise<(dockerRun: string) => string> { + if (!cachedComposerize) { + cachedComposerize = (await import('composerize')).default; + } + return cachedComposerize; +} + export const convertRouter = Router(); convertRouter.post('/', authMiddleware, async (req: Request, res: Response): Promise => { @@ -30,6 +38,7 @@ convertRouter.post('/', authMiddleware, async (req: Request, res: Response): Pro let yaml: unknown; try { + const composerize = await loadComposerize(); yaml = composerize(trimmed); } catch (error) { console.error('Conversion error:', error); diff --git a/backend/src/services/GitSourceService.ts b/backend/src/services/GitSourceService.ts index 848370ac..5649dd85 100644 --- a/backend/src/services/GitSourceService.ts +++ b/backend/src/services/GitSourceService.ts @@ -3,8 +3,6 @@ import { spawn } from 'child_process'; import crypto from 'crypto'; import os from 'os'; import path from 'path'; -import git from 'isomorphic-git'; -import gitHttp from 'isomorphic-git/http/node'; import YAML from 'yaml'; import { CryptoService } from './CryptoService'; import { DatabaseService, type StackGitSource, type GitSourceAuthType } from './DatabaseService'; @@ -13,6 +11,27 @@ import { ComposeService } from './ComposeService'; import { isDebugEnabled } from '../utils/debug'; import { sanitizeForLog } from '../utils/safeLog'; +// isomorphic-git is the heaviest dependency in the backend (~5 MB) and only +// fires when a stack is created from a Git source. Lazy-load it so cold +// boots without any Git-sourced stacks never parse the module. +type IsomorphicGit = typeof import('isomorphic-git')['default']; +type IsomorphicGitHttp = typeof import('isomorphic-git/http/node')['default']; + +let cachedGit: IsomorphicGit | undefined; +let cachedGitHttp: IsomorphicGitHttp | undefined; + +async function loadIsomorphicGit(): Promise<{ git: IsomorphicGit; gitHttp: IsomorphicGitHttp }> { + if (!cachedGit || !cachedGitHttp) { + const [gitMod, gitHttpMod] = await Promise.all([ + import('isomorphic-git'), + import('isomorphic-git/http/node'), + ]); + cachedGit = gitMod.default; + cachedGitHttp = gitHttpMod.default; + } + return { git: cachedGit, gitHttp: cachedGitHttp }; +} + /** * GitSourceService - fetch compose files from a Git repository and apply * them to local stacks. Tokens are encrypted via CryptoService. Shallow @@ -393,6 +412,7 @@ export class GitSourceService { : undefined; try { + const { git, gitHttp } = await loadIsomorphicGit(); // isomorphic-git does not natively accept an AbortSignal, so we // wrap the clone in a Promise.race against a timeout rejection. // The clone will keep running in the background until the socket