@@ -58,12 +58,12 @@ function collectCssFromManifest(entryKey, collected = new Set()) {
5858
5959 // Add CSS from this entry
6060 if ( entry . css ) {
61- entry . css . forEach ( css => collected . add ( css ) ) ;
61+ entry . css . forEach ( ( css ) => collected . add ( css ) ) ;
6262 }
6363
6464 // Recursively collect from imports
6565 if ( entry . imports ) {
66- entry . imports . forEach ( importKey => {
66+ entry . imports . forEach ( ( importKey ) => {
6767 if ( ! collected . has ( `visited:${ importKey } ` ) ) {
6868 collected . add ( `visited:${ importKey } ` ) ;
6969 collectCssFromManifest ( importKey , collected ) ;
@@ -72,7 +72,7 @@ function collectCssFromManifest(entryKey, collected = new Set()) {
7272 }
7373
7474 // Filter out visited markers and return just CSS paths
75- return Array . from ( collected ) . filter ( item => ! item . startsWith ( "visited:" ) ) ;
75+ return Array . from ( collected ) . filter ( ( item ) => ! item . startsWith ( "visited:" ) ) ;
7676}
7777
7878// Shared Vite plugin configuration
@@ -167,13 +167,13 @@ const layoutFiles = loadLayoutFiles(LAYOUT_DIR);
167167
168168// CSS collection with caching for performance
169169// Caches are invalidated when Vite detects file changes via HMR
170- const cssCache = new Map ( ) ; // filePath -> css string
171- const importsCache = new Map ( ) ; // filePath -> array of import paths
170+ const cssCache = new Map ( ) ; // filePath -> css string
171+ const importsCache = new Map ( ) ; // filePath -> array of import paths
172172
173173// Clear caches when files change (Vite HMR)
174174if ( dev && vite ) {
175- vite . watcher . on ( ' change' , ( filePath ) => {
176- if ( filePath . endsWith ( ' .svelte' ) ) {
175+ vite . watcher . on ( " change" , ( filePath ) => {
176+ if ( filePath . endsWith ( " .svelte" ) ) {
177177 cssCache . delete ( filePath ) ;
178178 importsCache . delete ( filePath ) ;
179179 }
@@ -193,7 +193,7 @@ async function collectComponentCss(componentPath) {
193193 try {
194194 const result = await vite . transformRequest ( cssUrl ) ;
195195 if ( result ?. code ) {
196- let css = '' ;
196+ let css = "" ;
197197 const viteMatch = result . code . match ( / c o n s t _ _ v i t e _ _ c s s \s * = \s * " ( (?: [ ^ " \\ ] | \\ .) * ) " / s) ;
198198 if ( viteMatch ) {
199199 css = viteMatch [ 1 ] ;
@@ -206,22 +206,24 @@ async function collectComponentCss(componentPath) {
206206 if ( css ) {
207207 // Unescape the CSS string
208208 css = css
209- . replace ( / \\ r / g, '' ) // Remove carriage returns
210- . replace ( / \\ n / g, '\n' )
211- . replace ( / \\ t / g, '\t' )
209+ . replace ( / \\ r / g, "" ) // Remove carriage returns
210+ . replace ( / \\ n / g, "\n" )
211+ . replace ( / \\ t / g, "\t" )
212212 . replace ( / \\ " / g, '"' )
213- . replace ( / \\ \\ / g, '\\' ) ;
213+ . replace ( / \\ \\ / g, "\\" ) ;
214214
215215 // Validate this is actually CSS, not component source
216216 // Vite returns full component source for styleless components
217217 const trimmed = css . trim ( ) ;
218- if ( trimmed . startsWith ( '<' ) ||
219- trimmed . startsWith ( '<!--' ) ||
220- trimmed . includes ( '<script>' ) ||
221- trimmed . includes ( '<svg' ) ) {
218+ if (
219+ trimmed . startsWith ( "<" ) ||
220+ trimmed . startsWith ( "<!--" ) ||
221+ trimmed . includes ( "<script>" ) ||
222+ trimmed . includes ( "<svg" )
223+ ) {
222224 // This is component markup, not CSS - skip it
223- cssCache . set ( svelteFilePath , '' ) ;
224- return '' ;
225+ cssCache . set ( svelteFilePath , "" ) ;
226+ return "" ;
225227 }
226228
227229 cssCache . set ( svelteFilePath , css ) ;
@@ -231,8 +233,8 @@ async function collectComponentCss(componentPath) {
231233 } catch ( e ) {
232234 // Component might not have styles
233235 }
234- cssCache . set ( svelteFilePath , '' ) ;
235- return '' ;
236+ cssCache . set ( svelteFilePath , "" ) ;
237+ return "" ;
236238 }
237239
238240 function getImportsFromSource ( filePath ) {
@@ -242,16 +244,16 @@ async function collectComponentCss(componentPath) {
242244 }
243245
244246 try {
245- const source = readFileSync ( filePath , ' utf-8' ) ;
247+ const source = readFileSync ( filePath , " utf-8" ) ;
246248 const imports = [ ] ;
247249 const importRegex = / i m p o r t \s + [ \w \s { } , * ] + \s + f r o m \s + [ " ' ] ( [ ^ " ' ] + \. s v e l t e ) [ " ' ] / g;
248250 let match ;
249251 while ( ( match = importRegex . exec ( source ) ) !== null ) {
250252 const importPath = match [ 1 ] ;
251- const fileDir = resolve ( filePath , '..' ) ;
252- if ( importPath . startsWith ( './' ) || importPath . startsWith ( ' ../' ) ) {
253+ const fileDir = resolve ( filePath , ".." ) ;
254+ if ( importPath . startsWith ( "./" ) || importPath . startsWith ( " ../" ) ) {
253255 imports . push ( resolve ( fileDir , importPath ) ) ;
254- } else if ( importPath . startsWith ( '/' ) ) {
256+ } else if ( importPath . startsWith ( "/" ) ) {
255257 imports . push ( join ( COMPONENT_DIR , importPath ) ) ;
256258 }
257259 }
@@ -265,11 +267,11 @@ async function collectComponentCss(componentPath) {
265267
266268 // Collect all component paths first (synchronous, uses cached imports)
267269 function collectAllPaths ( filePath ) {
268- const normalizedPath = filePath . split ( '?' ) [ 0 ] ;
270+ const normalizedPath = filePath . split ( "?" ) [ 0 ] ;
269271 if ( visited . has ( normalizedPath ) ) return ;
270272 visited . add ( normalizedPath ) ;
271273
272- if ( normalizedPath . endsWith ( ' .svelte' ) ) {
274+ if ( normalizedPath . endsWith ( " .svelte" ) ) {
273275 const imports = getImportsFromSource ( normalizedPath ) ;
274276 for ( const importPath of imports ) {
275277 collectAllPaths ( importPath ) ;
@@ -280,11 +282,9 @@ async function collectComponentCss(componentPath) {
280282 collectAllPaths ( componentPath ) ;
281283
282284 // Fetch all CSS in parallel
283- const cssResults = await Promise . all (
284- Array . from ( visited ) . map ( path => getCssForComponent ( path ) )
285- ) ;
285+ const cssResults = await Promise . all ( Array . from ( visited ) . map ( ( path ) => getCssForComponent ( path ) ) ) ;
286286
287- return cssResults . filter ( css => css ) . join ( '\n' ) ;
287+ return cssResults . filter ( ( css ) => css ) . join ( "\n" ) ;
288288}
289289
290290// Find the svelte runtime entry in manifest
@@ -398,10 +398,7 @@ async function handleRender(req) {
398398 } else {
399399 // Production: load pre-built SSR module
400400 const ssrPath = join ( BUILD_DIR , "server" , pathname . replace ( / \. s v e l t e $ / , ".js" ) ) ;
401- const [ componentModule , svelteModuleLoaded ] = await Promise . all ( [
402- import ( ssrPath ) ,
403- import ( "svelte/server" ) ,
404- ] ) ;
401+ const [ componentModule , svelteModuleLoaded ] = await Promise . all ( [ import ( ssrPath ) , import ( "svelte/server" ) ] ) ;
405402 component = componentModule . default ;
406403 svelteModule = svelteModuleLoaded ;
407404 render = svelteModule . render ;
@@ -438,7 +435,7 @@ async function handleRender(req) {
438435 injectHead += `<style>${ css } </style>` ;
439436 }
440437 } catch ( e ) {
441- console . error ( ' [vite-ssr] Failed to collect CSS:' , e . message ) ;
438+ console . error ( " [vite-ssr] Failed to collect CSS:" , e . message ) ;
442439 }
443440 } else if ( manifest ) {
444441 // Production: inject CSS links from manifest
@@ -461,6 +458,33 @@ async function handleRender(req) {
461458 } ) ;
462459}
463460
461+ // Memory management: run GC during idle periods to prevent memory accumulation
462+ let idleGcTimer = null ;
463+ const IDLE_GC_DELAY_MS = 5000 ; // Run GC after 5 seconds of no requests
464+ let requestsInFlight = 0 ;
465+
466+ function scheduleIdleGc ( ) {
467+ // Clear any existing timer
468+ if ( idleGcTimer ) {
469+ clearTimeout ( idleGcTimer ) ;
470+ idleGcTimer = null ;
471+ }
472+
473+ // Only schedule GC in production and when no requests are in flight
474+ if ( requestsInFlight > 0 ) return ;
475+
476+ idleGcTimer = setTimeout ( ( ) => {
477+ if ( requestsInFlight === 0 && typeof Bun !== "undefined" && Bun . gc ) {
478+ const before = process . memoryUsage ( ) ;
479+ Bun . gc ( true ) ; // Synchronous full GC
480+ const after = process . memoryUsage ( ) ;
481+ const freedMB = Math . round ( ( before . heapUsed - after . heapUsed ) / 1024 / 1024 ) ;
482+ console . log ( `[vite-ssr] Idle GC freed ${ freedMB } MB (heap: ${ Math . round ( after . heapUsed / 1024 / 1024 ) } MB)` ) ;
483+ }
484+ idleGcTimer = null ;
485+ } , IDLE_GC_DELAY_MS ) ;
486+ }
487+
464488// Start Bun HTTP server
465489const server = Bun . serve ( {
466490 port : parseInt ( NODE_PORT , 10 ) ,
@@ -473,17 +497,27 @@ const server = Bun.serve({
473497 return new Response ( "OK" , { status : 200 } ) ;
474498 }
475499
500+ // Track requests in flight
501+ requestsInFlight ++ ;
502+
476503 // Handle render requests
477504 const response = await handleRender ( req ) ;
478505
479506 const duration = ( performance . now ( ) - start ) . toFixed ( 2 ) ;
480507 console . log ( `[vite-ssr] ${ req . method } ${ url . pathname } ${ response . status } - ${ duration } ms` ) ;
481508
509+ // Decrement and schedule GC when idle
510+ requestsInFlight -- ;
511+ scheduleIdleGc ( ) ;
512+
482513 return response ;
483514 } ,
484515} ) ;
485516
486517console . log ( `[vite-ssr] Svelte SSR renderer listening in ${ NODE_ENV } mode on port ${ server . port } ` ) ;
518+ if ( isProductionBuild ) {
519+ console . log ( `[vite-ssr] Idle GC enabled: will run after ${ IDLE_GC_DELAY_MS } ms of inactivity` ) ;
520+ }
487521
488522// Also start Vite dev server for client HMR in development
489523if ( dev ) {
@@ -506,4 +540,4 @@ if (dev) {
506540 } ) ;
507541 await viteClientServer . listen ( ) ;
508542 console . log ( `[vite-ssr] Vite dev server running on ${ devProtocol } ://localhost:${ VITE_PORT } ` ) ;
509- }
543+ }
0 commit comments