@@ -19,10 +19,23 @@ function routeToFile(route) {
1919 return join ( DIST , route . replace ( / ^ \/ / , '' ) , 'index.html' ) ;
2020}
2121
22+ function fmtMs ( ms ) {
23+ if ( ms < 1000 ) return `${ ms } ms` ;
24+ return `${ ( ms / 1000 ) . toFixed ( 2 ) } s` ;
25+ }
26+
27+ function fmtSec ( ms ) {
28+ const s = ms / 1000 ;
29+ if ( s < 60 ) return `${ s . toFixed ( 2 ) } s` ;
30+ const m = Math . floor ( s / 60 ) ;
31+ return `${ m } m ${ ( s - m * 60 ) . toFixed ( 1 ) } s` ;
32+ }
33+
2234async function crawlOne ( browser , baseUrl , route ) {
35+ const t0 = Date . now ( ) ;
2336 const target = baseUrl + route ;
2437 const file = routeToFile ( route ) ;
25- if ( ! existsSync ( file ) ) return { route, skipped : 'no prerendered shell' } ;
38+ if ( ! existsSync ( file ) ) return { route, skipped : 'no prerendered shell' , ms : Date . now ( ) - t0 } ;
2639
2740 const page = await browser . newPage ( ) ;
2841 try {
@@ -40,7 +53,7 @@ async function crawlOne(browser, baseUrl, route) {
4053 try {
4154 await page . waitForSelector ( '#react-root > *' , { timeout : SELECTOR_TIMEOUT } ) ;
4255 } catch {
43- return { route, skipped : `no children rendered within ${ SELECTOR_TIMEOUT } ms` } ;
56+ return { route, skipped : `no children rendered within ${ SELECTOR_TIMEOUT } ms` , ms : Date . now ( ) - t0 } ;
4457 }
4558 await new Promise ( ( r ) => setTimeout ( r , RENDER_SETTLE_MS ) ) ;
4659
@@ -49,26 +62,50 @@ async function crawlOne(browser, baseUrl, route) {
4962 return root ? root . innerHTML : '' ;
5063 } ) ;
5164
52- if ( ! rendered ) return { route, skipped : 'empty root after render' } ;
65+ if ( ! rendered ) return { route, skipped : 'empty root after render' , ms : Date . now ( ) - t0 } ;
5366
5467 const shell = await readFile ( file , 'utf8' ) ;
5568 const replaced = shell . replace (
5669 / < d i v i d = " r e a c t - r o o t " > < \/ d i v > / ,
5770 `<div id="react-root">${ rendered } </div>`
5871 ) ;
5972 if ( replaced === shell ) {
60- return { route, skipped : 'root placeholder not found' } ;
73+ return { route, skipped : 'root placeholder not found' , ms : Date . now ( ) - t0 } ;
6174 }
6275 await writeFile ( file , replaced , 'utf8' ) ;
63- return { route, bytes : rendered . length , errors : consoleErrors . length } ;
76+ return { route, bytes : rendered . length , errors : consoleErrors . length , ms : Date . now ( ) - t0 } ;
6477 } catch ( e ) {
65- return { route, error : e . message } ;
78+ return { route, error : e . message , ms : Date . now ( ) - t0 } ;
6679 } finally {
6780 await page . close ( ) . catch ( ( ) => { } ) ;
6881 }
6982}
7083
84+ async function warmUp ( baseUrl ) {
85+ const deadline = Date . now ( ) + 10000 ;
86+ while ( Date . now ( ) < deadline ) {
87+ try {
88+ const res = await fetch ( baseUrl + '/' ) ;
89+ if ( res . ok ) return Date . now ( ) ;
90+ } catch { }
91+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
92+ }
93+ throw new Error ( 'preview server did not respond within 10s' ) ;
94+ }
95+
96+ function logRoute ( r ) {
97+ const t = fmtMs ( r . ms ?? 0 ) ;
98+ if ( r . error ) {
99+ console . log ( ` x ${ r . route } : ${ r . error } (${ t } )` ) ;
100+ } else if ( r . skipped ) {
101+ console . log ( ` - ${ r . route } : ${ r . skipped } (${ t } )` ) ;
102+ } else {
103+ console . log ( ` . ${ r . route } (${ r . bytes } bytes, ${ r . errors } console errors, ${ t } )` ) ;
104+ }
105+ }
106+
71107async function main ( ) {
108+ const tStart = Date . now ( ) ;
72109 console . log ( `prerender-crawl: ${ allRoutes . length } routes, concurrency ${ CONCURRENCY } ` ) ;
73110
74111 const server = await preview ( {
@@ -77,6 +114,10 @@ async function main() {
77114 const url = server . resolvedUrls ?. local ?. [ 0 ] ?. replace ( / \/ $ / , '' ) || 'http://127.0.0.1:4287' ;
78115 console . log ( `preview up at ${ url } ` ) ;
79116
117+ const warmStart = Date . now ( ) ;
118+ await warmUp ( url ) ;
119+ console . log ( `warm-up: preview responded in ${ fmtMs ( Date . now ( ) - warmStart ) } ` ) ;
120+
80121 const browser = await puppeteer . launch ( {
81122 args : [ '--no-sandbox' , '--disable-setuid-sandbox' ] ,
82123 } ) ;
@@ -88,13 +129,7 @@ async function main() {
88129 const route = queue . shift ( ) ;
89130 const r = await crawlOne ( browser , url , route ) ;
90131 results . push ( r ) ;
91- if ( r . error ) {
92- console . log ( ` x ${ route } : ${ r . error } ` ) ;
93- } else if ( r . skipped ) {
94- console . log ( ` - ${ route } : ${ r . skipped } ` ) ;
95- } else {
96- console . log ( ` . ${ route } (${ r . bytes } bytes, ${ r . errors } console errors)` ) ;
97- }
132+ logRoute ( r ) ;
98133 }
99134 } ) ;
100135 await Promise . all ( workers ) ;
@@ -109,9 +144,7 @@ async function main() {
109144 for ( const route of flaky ) {
110145 const r = await crawlOne ( browser , url , route ) ;
111146 retryResults . set ( route , r ) ;
112- if ( r . bytes ) console . log ( ` . ${ route } (retry: ${ r . bytes } bytes, ${ r . errors } console errors)` ) ;
113- else if ( r . skipped ) console . log ( ` - ${ route } : retry ${ r . skipped } ` ) ;
114- else if ( r . error ) console . log ( ` x ${ route } : retry ${ r . error } ` ) ;
147+ logRoute ( { ...r , route : `${ route } [retry]` } ) ;
115148 }
116149 for ( let i = 0 ; i < results . length ; i ++ ) {
117150 const r = results [ i ] ;
@@ -126,17 +159,25 @@ async function main() {
126159 await browser . close ( ) ;
127160 await new Promise ( ( resolve ) => server . httpServer . close ( resolve ) ) ;
128161
162+ const total = results . length ;
129163 const ok = results . filter ( ( r ) => r . bytes ) . length ;
130164 const errs = results . filter ( ( r ) => r . error ) . length ;
131165 const skipped = results . filter ( ( r ) => r . skipped ) . length ;
132- console . log ( `\nprerender-crawl: ${ ok } rendered, ${ skipped } skipped, ${ errs } errored` ) ;
166+ const routeTimes = results . map ( ( r ) => r . ms || 0 ) ;
167+ const avgMs = routeTimes . length ? routeTimes . reduce ( ( a , b ) => a + b , 0 ) / routeTimes . length : 0 ;
168+ const maxMs = routeTimes . length ? Math . max ( ...routeTimes ) : 0 ;
169+ const pct = total ? ( ( ok / total ) * 100 ) . toFixed ( 1 ) : '0.0' ;
170+ const elapsed = Date . now ( ) - tStart ;
171+
172+ console . log ( `\nprerender-crawl: ${ ok } /${ total } rendered (${ pct } % success), ${ skipped } skipped, ${ errs } errored` ) ;
173+ console . log ( `prerender-crawl: per-route avg ${ fmtMs ( Math . round ( avgMs ) ) } , max ${ fmtMs ( maxMs ) } ` ) ;
174+ console . log ( `prerender-crawl: total elapsed ${ fmtSec ( elapsed ) } ` ) ;
133175
134176 try {
135177 const home = await readFile ( join ( DIST , 'index.html' ) , 'utf8' ) ;
136178 await writeFile ( join ( DIST , '404.html' ) , home , 'utf8' ) ;
137179 } catch { }
138180
139- // Skipped routes keep their empty-shell HTML + SPA fallback — not a build failure.
140181 if ( errs > 0 ) process . exit ( 1 ) ;
141182}
142183
0 commit comments