import {chromium} from 'playwright'; import {mkdir,writeFile} from 'node:fs/promises'; import {dirname,join} from 'node:path'; import {cpus,platform,arch} from 'node:os'; // Initial-load laboratory samples, not a Lighthouse or field-data score. // Requires Node.js and Playwright 1.62.1 with its Chromium installation. // node benchmark.mjs --samples 3 --profiles mobile,desktop --out results.json // Both profiles retain Chromium's desktop user agent. Mobile emulates viewport, // touch, network and CPU, not a physical phone or a mobile browser binary. const args=process.argv.slice(2),arg=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback; const output=arg('--out','benchmark-results.json'),samples=Number(arg('--samples','3')),windowMs=Number(arg('--window','20000')); const profiles={mobile:{viewport:{width:390,height:844},downloadMbps:1.6,uploadMbps:.75,latencyMs:150,cpuSlowdown:4,deviceScaleFactor:1,isMobile:true},desktop:{viewport:{width:1440,height:1000},downloadMbps:20,uploadMbps:10,latencyMs:40,cpuSlowdown:1,deviceScaleFactor:1,isMobile:false}}; const targets=[{id:'astra',name:'Astra',url:arg('--astra','https://iamtop.cat/astra/')},{id:'renfe',name:'Renfe',url:'https://www.renfe.com/es/es'}]; const browser=await chromium.launch({headless:true,args:['--no-sandbox']}); const report={schema:1,startedAt:new Date().toISOString(),browser:browser.version(),environment:{os:platform(),arch:arch(),cpu:cpus()[0]?.model,logicalCpus:cpus().length,location:'Local test machine; network route and server distance are not controlled'},method:{name:'Initial-page-load lab test',samplesPerProfile:samples,windowMs,coldCache:true,cookies:'Fresh context, consent untouched',order:'Alternating Astra/Renfe order by repetition',interaction:'No scrolling, searches, login or purchases',limits:'Small lab sample, not field Core Web Vitals, INP, availability monitoring, a Lighthouse score or a comparison of booking backends. Astra is a local-data demo; Renfe sells real journeys.'},profiles,targets,runs:[]}; await mkdir(dirname(output),{recursive:true}); async function measure(target,profile,index){ const config=profiles[profile],context=await browser.newContext({viewport:config.viewport,deviceScaleFactor:config.deviceScaleFactor,isMobile:config.isMobile,hasTouch:config.isMobile,locale:'es-ES',timezoneId:'Europe/Madrid',serviceWorkers:'block'}),page=await context.newPage(),cdp=await context.newCDPSession(page); const run={target:target.id,profile,index,startedAt:new Date().toISOString(),status:'incomplete',httpStatus:null,requests:0,failedRequests:0,bytes:0,metrics:null}; let started=0; await cdp.send('Network.enable');await cdp.send('Network.setCacheDisabled',{cacheDisabled:true}); await cdp.send('Network.emulateNetworkConditions',{offline:false,latency:config.latencyMs,downloadThroughput:config.downloadMbps*1000000/8,uploadThroughput:config.uploadMbps*1000000/8,connectionType:config.isMobile?'cellular4g':'ethernet'}); await cdp.send('Emulation.setCPUThrottlingRate',{rate:config.cpuSlowdown}); cdp.on('Network.requestWillBeSent',()=>{if(started&&Date.now()-started<=windowMs)run.requests++;}); cdp.on('Network.loadingFinished',event=>{if(started&&Date.now()-started<=windowMs)run.bytes+=event.encodedDataLength;}); cdp.on('Network.loadingFailed',()=>{if(started&&Date.now()-started<=windowMs)run.failedRequests++;}); await page.addInitScript(({windowMs})=>{ window.__astraLab={lcp:null,cls:0,longTaskMs:0,longTaskCount:0};let sessionValue=0,firstShift=0,lastShift=0; new PerformanceObserver(list=>{for(const e of list.getEntries())if(e.startTime<=windowMs)window.__astraLab.lcp=e.startTime;}).observe({type:'largest-contentful-paint',buffered:true}); new PerformanceObserver(list=>{for(const e of list.getEntries())if(!e.hadRecentInput&&e.startTime<=windowMs){if(e.startTime-lastShift<1000&&e.startTime-firstShift<5000)sessionValue+=e.value;else{sessionValue=e.value;firstShift=e.startTime;}lastShift=e.startTime;window.__astraLab.cls=Math.max(window.__astraLab.cls,sessionValue);}}).observe({type:'layout-shift',buffered:true}); new PerformanceObserver(list=>{for(const e of list.getEntries())if(e.startTime<=windowMs){window.__astraLab.longTaskMs+=Math.min(e.duration,windowMs-e.startTime);window.__astraLab.longTaskCount++;}}).observe({type:'longtask',buffered:true}); },{windowMs}); try{ started=Date.now();const response=await page.goto(target.url,{waitUntil:'commit',timeout:30000});run.httpStatus=response?.status(); await page.waitForTimeout(Math.max(0,windowMs-(Date.now()-started))); const info=await page.evaluate(({windowMs})=>{const n=performance.getEntriesByType('navigation')[0],fcp=performance.getEntriesByName('first-contentful-paint')[0]?.startTime;return {title:document.title,url:location.href,text:document.body?.innerText.slice(0,800)||'',metrics:{ttfbMs:n?.responseStart||null,fcpMs:fcp<=windowMs?fcp:null,lcpMs:window.__astraLab?.lcp??null,cls:window.__astraLab?.cls??null,longTaskMs:window.__astraLab?.longTaskMs??null,longTaskCount:window.__astraLab?.longTaskCount??null,domContentLoadedMs:n?.domContentLoadedEventEnd>0&&n.domContentLoadedEventEnd<=windowMs?n.domContentLoadedEventEnd:null,loadMs:n?.loadEventEnd>0&&n.loadEventEnd<=windowMs?n.loadEventEnd:null}};},{windowMs}); run.title=info.title;run.finalUrl=info.url;run.excerpt=info.text;run.metrics=info.metrics; const blocked=/access denied|request rejected|verify (that )?you are human|just a moment|unusual traffic/i.test(info.title+' '+info.text); run.status=blocked||run.httpStatus===403||run.httpStatus===429?'blocked':run.httpStatus>=400?'http-error':info.metrics.fcpMs===null?'no-paint':'measured'; await page.screenshot({path:join(dirname(output),`${target.id}-${profile}-${index}.png`)}); }catch(error){run.error=String(error.message).slice(0,500);} finally{run.observedWallMs=Date.now()-started;await context.close();} return run; } try{ for(const profile of arg('--profiles','mobile,desktop').split(',')){ if(!profiles[profile])throw Error('Unknown profile'); for(let index=1;index<=samples;index++)for(const target of index%2?targets:[...targets].reverse()){ const run=await measure(target,profile,index);report.runs.push(run);await writeFile(output,JSON.stringify(report,null,2)+'\n'); console.log(JSON.stringify({target:run.target,profile,index,status:run.status,http:run.httpStatus,bytes:run.bytes,metrics:run.metrics})); } } }finally{report.completedAt=new Date().toISOString();await writeFile(output,JSON.stringify(report,null,2)+'\n');await browser.close();}