diff --git a/.github/workflows/bundle-size-check.yml b/.github/workflows/bundle-size-check.yml new file mode 100644 index 0000000000..99440b42aa --- /dev/null +++ b/.github/workflows/bundle-size-check.yml @@ -0,0 +1,103 @@ +name: Bundle Size Check + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + check-bundle-size: + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup and build current branch + uses: ./.github/actions/prepare-playground + + - name: Build website + run: npx nx build playground-website + + - name: Start preview server + run: | + npx nx preview playground-website & + # Wait for server to be ready + timeout 60 bash -c 'until curl -s http://localhost:5400 > /dev/null; do sleep 1; done' + + - name: Install Playwright browsers + run: npx playwright install chromium --with-deps + + - name: Measure current bundle size + run: node tools/scripts/measure-bundle-size-browser.mjs + + - name: Stop preview server + run: pkill -f "nx preview" || true + + - name: Save current report + run: cp bundle-size-report.json bundle-size-report-current.json + + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + submodules: true + + - name: Setup and build base branch + uses: ./.github/actions/prepare-playground + + - name: Build base branch website + run: npx nx build playground-website + + - name: Start preview server for base branch + run: | + npx nx preview playground-website & + # Wait for server to be ready + timeout 60 bash -c 'until curl -s http://localhost:5400 > /dev/null; do sleep 1; done' + + - name: Measure base bundle size + run: node tools/scripts/measure-bundle-size-browser.mjs + + - name: Stop preview server + run: pkill -f "nx preview" || true + + - name: Save base report + run: cp bundle-size-report.json bundle-size-report-base.json + + - name: Restore current report + run: cp bundle-size-report-current.json bundle-size-report.json + + - name: Compare bundle sizes + id: compare + run: node tools/scripts/compare-bundle-size.mjs bundle-size-report-base.json bundle-size-report.json + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: '## 📦 Bundle Size Report' + + - name: Create or update comment + if: steps.compare.outputs.should_comment == 'true' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: bundle-size-comment.md + edit-mode: replace + + - name: Upload bundle size reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: bundle-size-reports + path: | + bundle-size-report.json + bundle-size-report-base.json + bundle-size-comment.md diff --git a/tools/scripts/README.md b/tools/scripts/README.md new file mode 100644 index 0000000000..8231c3d12c --- /dev/null +++ b/tools/scripts/README.md @@ -0,0 +1,191 @@ +# Bundle Size Tracking + +This directory contains scripts for tracking and reporting bundle size changes in WordPress Playground using real browser measurements. + +## Overview + +The bundle size tracking system uses Playwright to measure actual download sizes at key stages during page load: + +1. **First Paint**: Assets downloaded until the progress bar is visible +2. **WordPress Loaded**: Assets downloaded until WordPress site is ready (nested iframes loaded) +3. **Offline Mode Ready**: All assets downloaded after network activity settles + +This approach provides real-world measurements instead of static file analysis. + +## Scripts + +### `measure-bundle-size-browser.mjs` + +Uses Playwright to measure bundle size by monitoring actual browser network requests. + +**Usage:** +```bash +# Start the development server +npm run dev + +# In another terminal, run the measurement +node tools/scripts/measure-bundle-size-browser.mjs +``` + +**What it measures:** +- Total bytes transferred at each stage +- Number of files loaded +- Time to each milestone +- Top 10 largest files at each stage +- Breakdown by resource type (script, stylesheet, image, etc.) + +**Output:** +- `bundle-size-report.json`: Detailed JSON report with measurements + +### `compare-bundle-size.mjs` + +Compares two bundle size reports and generates a markdown report suitable for GitHub PR comments. + +**Usage:** +```bash +node tools/scripts/compare-bundle-size.mjs [base-report] [current-report] +``` + +**Default paths:** +- `base-report`: `bundle-size-report-base.json` +- `current-report`: `bundle-size-report.json` + +**Output:** +- `bundle-size-comment.md`: Markdown-formatted comparison report +- GitHub Actions outputs for workflow automation + +## CI Workflow + +The bundle size check runs automatically on pull requests via the `.github/workflows/bundle-size-check.yml` workflow. + +### How it works + +1. **Build & Start Current Branch**: + - Builds the website from the PR branch + - Starts the preview server + - Installs Playwright + - Measures bundle size with real browser + +2. **Build & Start Base Branch**: + - Checks out and builds the base branch (usually `trunk`) + - Starts the preview server + - Measures its bundle size with real browser + +3. **Compare**: + - Generates a comparison report showing size changes at each stage + - Includes time delta as well as size delta + +4. **Comment**: + - If any stage changes by more than 50 KB, posts a comment on the PR + +### Comment Threshold + +A PR comment is posted when any of these change by more than ±50 KB: +- First paint downloads +- WordPress loaded downloads +- Offline mode ready downloads + +### Comment Format + +The PR comment includes three sections: + +#### 🎨 First Paint (Progress Bar Visible) +- Current vs. base size and load time +- Delta in bytes and time +- Top 10 largest files + +#### ✅ WordPress Loaded (Site Ready) +- Current vs. base size and load time +- Delta in bytes and time +- Top 10 largest files + +#### 💾 Offline Mode Ready (All Downloads Settled) +- Current vs. base size and load time +- Delta in bytes and time +- Top 10 largest files + +**Status Indicators**: +- 📈 Size increased +- 📉 Size decreased +- ➡️ No change + +## Measurement Stages + +### First Paint (Progress Bar Visible) + +Measures all downloads until the progress bar becomes visible. This represents the minimum assets needed for users to see that the page is loading. + +**Key signals:** +- Progress bar element visible +- Falls back to DOMContentLoaded if no progress bar found + +### WordPress Loaded (Site Ready) + +Measures all downloads until WordPress is fully loaded in the nested iframe, indicated by the WordPress admin bar being visible. + +**Key signals:** +- WordPress admin bar (`#wpadminbar`) is visible in the nested iframe +- Falls back to window load event if admin bar not found + +### Offline Mode Ready (All Downloads Settled) + +Measures all downloads after network activity settles (5 seconds of no new requests). This represents all assets that would be cached for offline use. + +**Key signals:** +- No network requests for 5 consecutive seconds +- Includes all lazy-loaded assets + +## Local Development + +To test bundle size changes locally: + +```bash +# Terminal 1: Start the dev server +npm run dev + +# Terminal 2: Measure current build +node tools/scripts/measure-bundle-size-browser.mjs + +# Save as base for comparison +cp bundle-size-report.json bundle-size-report-base.json + +# Make your changes... + +# Restart dev server if needed +npm run dev + +# Measure new build +node tools/scripts/measure-bundle-size-browser.mjs + +# Compare +node tools/scripts/compare-bundle-size.mjs +``` + +## Optimization Tips + +If your PR triggers a bundle size increase: + +1. **Check for new dependencies**: Large libraries can significantly increase bundle size +2. **Use code splitting**: Move non-critical code to lazy-loaded chunks +3. **Optimize assets**: Compress images, minify code +4. **Review network tab**: Use browser DevTools to see what's being loaded +5. **Consider alternatives**: Look for lighter-weight alternatives to heavy dependencies +6. **Analyze resource types**: Check if images, scripts, or styles are the main contributor + +## Browser-Based Measurement Benefits + +Using real browser measurements instead of static file analysis provides: + +- **Realistic data**: Measures what users actually download +- **Network behavior**: Captures caching, compression, and HTTP/2 multiplexing effects +- **Load timing**: Shows when assets are downloaded relative to page milestones +- **Resource prioritization**: Reflects browser's actual loading strategy +- **Accurate offline assets**: Measures what's actually cached, not estimates + +## Artifacts + +The workflow uploads the following artifacts for debugging: + +- `bundle-size-report.json`: Current branch measurements +- `bundle-size-report-base.json`: Base branch measurements +- `bundle-size-comment.md`: Generated PR comment diff --git a/tools/scripts/compare-bundle-size.mjs b/tools/scripts/compare-bundle-size.mjs new file mode 100755 index 0000000000..9e8f984d74 --- /dev/null +++ b/tools/scripts/compare-bundle-size.mjs @@ -0,0 +1,246 @@ +#!/usr/bin/env node + +/** + * Compare bundle sizes between current and base branch + * + * This script compares browser-based measurements from two builds + * and generates a markdown report for GitHub PR comments. + */ + +import { readFileSync, existsSync } from 'fs'; + +const THRESHOLD_KB = 50; // Threshold for posting a comment + +/** + * Format bytes to human readable format + */ +function formatBytes(bytes) { + if (bytes >= 1024 * 1024) { + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; + } + return `${(bytes / 1024).toFixed(2)} KB`; +} + +/** + * Format size delta with sign + */ +function formatDelta(delta) { + if (delta === 0) return '0 KB'; + const sign = delta > 0 ? '+' : ''; + if (Math.abs(delta) >= 1024 * 1024) { + return `${sign}${(delta / 1024 / 1024).toFixed(2)} MB`; + } + return `${sign}${(delta / 1024).toFixed(2)} KB`; +} + +/** + * Load a bundle report + */ +function loadReport(path) { + if (!existsSync(path)) { + return null; + } + return JSON.parse(readFileSync(path, 'utf-8')); +} + +/** + * Generate a markdown table for largest files + */ +function generateFileTable(files) { + if (!files || files.length === 0) { + return '_No files tracked_'; + } + + let table = '| File | Size | Type |\n'; + table += '|------|-----:|:----:|\n'; + + for (const file of files) { + const url = new URL(file.url); + const path = url.pathname.length > 60 + ? '...' + url.pathname.slice(-57) + : url.pathname; + table += `| \`${path}\` | ${formatBytes(file.size)} | ${file.resourceType} |\n`; + } + + return table; +} + +/** + * Compare two reports and generate markdown + */ +function compareReports(baseReport, currentReport) { + if (!baseReport) { + return { + shouldComment: true, + markdown: generateNewBuildReport(currentReport), + }; + } + + const base = baseReport.measurements; + const current = currentReport.measurements; + + // Calculate deltas + const firstPaintDelta = current.firstPaint.totalBytes - base.firstPaint.totalBytes; + const wpLoadedDelta = current.wordpressLoaded.totalBytes - base.wordpressLoaded.totalBytes; + const offlineModeDelta = current.offlineModeReady.totalBytes - base.offlineModeReady.totalBytes; + + // Determine if we should post a comment (50KB threshold) + const shouldComment = + Math.abs(firstPaintDelta) >= THRESHOLD_KB * 1024 || + Math.abs(wpLoadedDelta) >= THRESHOLD_KB * 1024 || + Math.abs(offlineModeDelta) >= THRESHOLD_KB * 1024; + + // Generate markdown + const markdown = generateComparisonReport( + base, + current, + firstPaintDelta, + wpLoadedDelta, + offlineModeDelta + ); + + return { + shouldComment, + markdown, + firstPaintDelta, + wpLoadedDelta, + offlineModeDelta, + }; +} + +/** + * Generate a report for a new build + */ +function generateNewBuildReport(report) { + const m = report.measurements; + + return `## 📦 Bundle Size Report + +### 🎨 First Paint (Progress Bar Visible) +- **Total Downloaded**: ${formatBytes(m.firstPaint.totalBytes)} +- **File Count**: ${m.firstPaint.fileCount} +- **Time**: ${m.firstPaint.timestamp}ms + +#### Top 10 Largest Files +${generateFileTable(m.firstPaint.largestFiles)} + +### ✅ WordPress Loaded (Site Ready) +- **Total Downloaded**: ${formatBytes(m.wordpressLoaded.totalBytes)} +- **File Count**: ${m.wordpressLoaded.fileCount} +- **Time**: ${m.wordpressLoaded.timestamp}ms + +#### Top 10 Largest Files +${generateFileTable(m.wordpressLoaded.largestFiles)} + +### 💾 Offline Mode Ready (All Downloads Settled) +- **Total Downloaded**: ${formatBytes(m.offlineModeReady.totalBytes)} +- **File Count**: ${m.offlineModeReady.fileCount} +- **Time**: ${m.offlineModeReady.timestamp}ms + +#### Top 10 Largest Files +${generateFileTable(m.offlineModeReady.largestFiles)} +`; +} + +/** + * Generate a comparison report + */ +function generateComparisonReport( + base, + current, + firstPaintDelta, + wpLoadedDelta, + offlineModeDelta +) { + const firstPaintEmoji = firstPaintDelta > 0 ? '📈' : firstPaintDelta < 0 ? '📉' : '➡️'; + const wpLoadedEmoji = wpLoadedDelta > 0 ? '📈' : wpLoadedDelta < 0 ? '📉' : '➡️'; + const offlineModeEmoji = offlineModeDelta > 0 ? '📈' : offlineModeDelta < 0 ? '📉' : '➡️'; + + return `## 📦 Bundle Size Report + +### ${firstPaintEmoji} First Paint (Progress Bar Visible) +- **Current**: ${formatBytes(current.firstPaint.totalBytes)} in ${current.firstPaint.timestamp}ms +- **Base**: ${formatBytes(base.firstPaint.totalBytes)} in ${base.firstPaint.timestamp}ms +- **Delta**: ${formatDelta(firstPaintDelta)} (${formatDelta(current.firstPaint.timestamp - base.firstPaint.timestamp)} time) +- **Files**: ${current.firstPaint.fileCount} (was ${base.firstPaint.fileCount}) + +#### Top 10 Largest Files +${generateFileTable(current.firstPaint.largestFiles)} + +### ${wpLoadedEmoji} WordPress Loaded (Site Ready) +- **Current**: ${formatBytes(current.wordpressLoaded.totalBytes)} in ${current.wordpressLoaded.timestamp}ms +- **Base**: ${formatBytes(base.wordpressLoaded.totalBytes)} in ${base.wordpressLoaded.timestamp}ms +- **Delta**: ${formatDelta(wpLoadedDelta)} (${formatDelta(current.wordpressLoaded.timestamp - base.wordpressLoaded.timestamp)} time) +- **Files**: ${current.wordpressLoaded.fileCount} (was ${base.wordpressLoaded.fileCount}) + +#### Top 10 Largest Files +${generateFileTable(current.wordpressLoaded.largestFiles)} + +### ${offlineModeEmoji} Offline Mode Ready (All Downloads Settled) +- **Current**: ${formatBytes(current.offlineModeReady.totalBytes)} in ${current.offlineModeReady.timestamp}ms +- **Base**: ${formatBytes(base.offlineModeReady.totalBytes)} in ${base.offlineModeReady.timestamp}ms +- **Delta**: ${formatDelta(offlineModeDelta)} (${formatDelta(current.offlineModeReady.timestamp - base.offlineModeReady.timestamp)} time) +- **Files**: ${current.offlineModeReady.fileCount} (was ${base.offlineModeReady.fileCount}) + +#### Top 10 Largest Files +${generateFileTable(current.offlineModeReady.largestFiles)} +`; +} + +/** + * Main comparison function + */ +async function main() { + const baseReportPath = process.argv[2] || 'bundle-size-report-base.json'; + const currentReportPath = process.argv[3] || 'bundle-size-report.json'; + + console.log('Comparing bundle sizes...'); + console.log(`Base report: ${baseReportPath}`); + console.log(`Current report: ${currentReportPath}`); + + const baseReport = loadReport(baseReportPath); + const currentReport = loadReport(currentReportPath); + + if (!currentReport) { + console.error(`Current report not found: ${currentReportPath}`); + process.exit(1); + } + + const comparison = compareReports(baseReport, currentReport); + + console.log('\n' + comparison.markdown); + + // Write markdown to file for GitHub Actions + const fs = await import('fs/promises'); + await fs.writeFile('bundle-size-comment.md', comparison.markdown); + + // Output results for GitHub Actions + console.log('\n=== GitHub Actions Output ==='); + console.log(`SHOULD_COMMENT=${comparison.shouldComment}`); + if (comparison.firstPaintDelta !== undefined) { + console.log(`FIRST_PAINT_DELTA=${comparison.firstPaintDelta}`); + } + if (comparison.wpLoadedDelta !== undefined) { + console.log(`WP_LOADED_DELTA=${comparison.wpLoadedDelta}`); + } + if (comparison.offlineModeDelta !== undefined) { + console.log(`OFFLINE_MODE_DELTA=${comparison.offlineModeDelta}`); + } + + // Set GitHub Actions output + if (process.env.GITHUB_OUTPUT) { + const output = [ + `should_comment=${comparison.shouldComment}`, + `first_paint_delta=${comparison.firstPaintDelta || 0}`, + `wp_loaded_delta=${comparison.wpLoadedDelta || 0}`, + `offline_mode_delta=${comparison.offlineModeDelta || 0}`, + ].join('\n'); + + await fs.appendFile(process.env.GITHUB_OUTPUT, output + '\n'); + } +} + +main().catch((error) => { + console.error('Error comparing bundles:', error); + process.exit(1); +}); diff --git a/tools/scripts/measure-bundle-size-browser.mjs b/tools/scripts/measure-bundle-size-browser.mjs new file mode 100755 index 0000000000..cf020cea84 --- /dev/null +++ b/tools/scripts/measure-bundle-size-browser.mjs @@ -0,0 +1,343 @@ +#!/usr/bin/env node + +/** + * Measure bundle size using actual browser and network monitoring + * + * This script uses Playwright to: + * 1. Launch the playground website + * 2. Track all network requests + * 3. Measure downloads at three key stages: + * - Until progress bar visible (first paint) + * - After WordPress loaded (site ready) + * - After all downloads settle (offline mode readiness) + */ + +import { chromium } from 'playwright'; +import { existsSync } from 'fs'; +import { writeFile } from 'fs/promises'; + +const WEBSITE_URL = 'http://localhost:5400'; +const NETWORK_IDLE_TIMEOUT = 5000; // 5 seconds of no network activity + +/** + * Track network requests and calculate total bytes transferred + */ +class NetworkMonitor { + constructor() { + this.requests = []; + this.responses = new Map(); + this.startTime = null; + } + + /** + * Attach to a page to monitor network activity + */ + attach(page) { + this.startTime = Date.now(); + + page.on('request', (request) => { + this.requests.push({ + url: request.url(), + method: request.method(), + resourceType: request.resourceType(), + timestamp: Date.now() - this.startTime, + }); + }); + + page.on('response', async (response) => { + const request = response.request(); + const url = request.url(); + + try { + const headers = response.headers(); + const contentLength = headers['content-length']; + const body = await response.body().catch(() => null); + + this.responses.set(url, { + url, + status: response.status(), + contentLength: contentLength + ? parseInt(contentLength, 10) + : null, + actualSize: body ? body.length : 0, + resourceType: request.resourceType(), + timestamp: Date.now() - this.startTime, + }); + } catch (error) { + // Some responses can't be read (e.g., service worker) + console.warn(`Could not read response for ${url}:`, error.message); + } + }); + } + + /** + * Get all responses up to a certain timestamp + */ + getResponsesUntil(timestamp) { + return Array.from(this.responses.values()).filter( + (r) => r.timestamp <= timestamp + ); + } + + /** + * Calculate total bytes transferred + */ + calculateTotalBytes(responses) { + return responses.reduce((total, response) => { + // Use actual size if available, fall back to content-length + const size = response.actualSize || response.contentLength || 0; + return total + size; + }, 0); + } + + /** + * Group responses by resource type + */ + groupByResourceType(responses) { + const groups = {}; + for (const response of responses) { + const type = response.resourceType || 'other'; + if (!groups[type]) { + groups[type] = []; + } + groups[type].push(response); + } + return groups; + } + + /** + * Get largest files + */ + getLargestFiles(responses, count = 10) { + return [...responses] + .sort((a, b) => { + const sizeA = a.actualSize || a.contentLength || 0; + const sizeB = b.actualSize || b.contentLength || 0; + return sizeB - sizeA; + }) + .slice(0, count) + .map((r) => ({ + url: r.url, + size: r.actualSize || r.contentLength || 0, + resourceType: r.resourceType, + })); + } +} + +/** + * Wait for network to be idle + */ +async function waitForNetworkIdle(page, timeout = NETWORK_IDLE_TIMEOUT) { + let lastRequestTime = Date.now(); + let requestCount = 0; + + const requestListener = () => { + lastRequestTime = Date.now(); + requestCount++; + }; + + page.on('request', requestListener); + + try { + // Wait for network idle + while (Date.now() - lastRequestTime < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } finally { + page.off('request', requestListener); + } + + return requestCount; +} + +/** + * Main measurement function + */ +async function measureBundleSize() { + console.log('Starting browser-based bundle size measurement...'); + + // Check if server is running + try { + const response = await fetch(WEBSITE_URL); + if (!response.ok) { + throw new Error(`Server returned ${response.status}`); + } + } catch (error) { + console.error(`Website not accessible at ${WEBSITE_URL}`); + console.error('Please start the dev server first: npm run dev'); + process.exit(1); + } + + const browser = await chromium.launch({ + headless: true, + }); + + const context = await browser.newContext({ + // Disable cache to get accurate measurements + ignoreHTTPSErrors: true, + }); + + const page = await context.newPage(); + + // Set up network monitoring + const monitor = new NetworkMonitor(); + monitor.attach(page); + + const measurements = {}; + + try { + // Navigate to the website + console.log(`Loading ${WEBSITE_URL}...`); + await page.goto(WEBSITE_URL, { + waitUntil: 'domcontentloaded', + }); + + // Stage 1: Wait for progress bar to be visible (first paint) + console.log('Waiting for progress bar...'); + try { + await page.waitForSelector('.progress-bar, [role="progressbar"]', { + timeout: 10000, + state: 'visible', + }); + const progressBarTime = Date.now() - monitor.startTime; + const progressBarResponses = + monitor.getResponsesUntil(progressBarTime); + + measurements.firstPaint = { + timestamp: progressBarTime, + totalBytes: monitor.calculateTotalBytes(progressBarResponses), + fileCount: progressBarResponses.length, + largestFiles: monitor.getLargestFiles(progressBarResponses), + byType: monitor.groupByResourceType(progressBarResponses), + }; + + console.log( + `Progress bar visible at ${progressBarTime}ms, ${measurements.firstPaint.totalBytes} bytes downloaded` + ); + } catch (error) { + console.warn('Progress bar not found, using DOM content loaded instead'); + const domContentLoadedTime = Date.now() - monitor.startTime; + const responses = monitor.getResponsesUntil(domContentLoadedTime); + + measurements.firstPaint = { + timestamp: domContentLoadedTime, + totalBytes: monitor.calculateTotalBytes(responses), + fileCount: responses.length, + largestFiles: monitor.getLargestFiles(responses), + byType: monitor.groupByResourceType(responses), + }; + } + + // Stage 2: Wait for WordPress to load (admin bar visible) + console.log('Waiting for WordPress to load...'); + try { + // Wait for the WordPress iframe (remote iframe -> WordPress iframe) + const wpFrame = page.frameLocator('#playground-viewport:visible').frameLocator('#wp'); + + // Wait for WordPress admin bar to be visible + await wpFrame.locator('#wpadminbar').waitFor({ + state: 'visible', + timeout: 30000, + }); + + const wpLoadedTime = Date.now() - monitor.startTime; + const wpLoadedResponses = monitor.getResponsesUntil(wpLoadedTime); + + measurements.wordpressLoaded = { + timestamp: wpLoadedTime, + totalBytes: monitor.calculateTotalBytes(wpLoadedResponses), + fileCount: wpLoadedResponses.length, + largestFiles: monitor.getLargestFiles(wpLoadedResponses), + byType: monitor.groupByResourceType(wpLoadedResponses), + }; + + console.log( + `WordPress loaded at ${wpLoadedTime}ms, ${measurements.wordpressLoaded.totalBytes} bytes downloaded` + ); + } catch (error) { + console.warn('WordPress admin bar not found:', error.message); + // Fall back to network load event + await page.waitForLoadState('load'); + const loadTime = Date.now() - monitor.startTime; + const responses = monitor.getResponsesUntil(loadTime); + + measurements.wordpressLoaded = { + timestamp: loadTime, + totalBytes: monitor.calculateTotalBytes(responses), + fileCount: responses.length, + largestFiles: monitor.getLargestFiles(responses), + byType: monitor.groupByResourceType(responses), + }; + } + + // Stage 3: Wait for all downloads to settle (offline mode ready) + console.log('Waiting for network to be idle...'); + await waitForNetworkIdle(page, NETWORK_IDLE_TIMEOUT); + + const networkIdleTime = Date.now() - monitor.startTime; + const allResponses = Array.from(monitor.responses.values()); + + measurements.offlineModeReady = { + timestamp: networkIdleTime, + totalBytes: monitor.calculateTotalBytes(allResponses), + fileCount: allResponses.length, + largestFiles: monitor.getLargestFiles(allResponses), + byType: monitor.groupByResourceType(allResponses), + }; + + console.log( + `Network idle at ${networkIdleTime}ms, ${measurements.offlineModeReady.totalBytes} bytes downloaded` + ); + + // Generate report + const report = { + timestamp: new Date().toISOString(), + url: WEBSITE_URL, + measurements, + }; + + // Write report to file + await writeFile( + 'bundle-size-report.json', + JSON.stringify(report, null, 2) + ); + + // Print summary + console.log('\n=== Bundle Size Report ===\n'); + console.log('First Paint (Progress Bar Visible):'); + console.log( + ` Total: ${(measurements.firstPaint.totalBytes / 1024 / 1024).toFixed(2)} MB` + ); + console.log(` Files: ${measurements.firstPaint.fileCount}`); + console.log(` Time: ${measurements.firstPaint.timestamp}ms`); + + console.log('\nWordPress Loaded (Site Ready):'); + console.log( + ` Total: ${(measurements.wordpressLoaded.totalBytes / 1024 / 1024).toFixed(2)} MB` + ); + console.log(` Files: ${measurements.wordpressLoaded.fileCount}`); + console.log(` Time: ${measurements.wordpressLoaded.timestamp}ms`); + + console.log('\nOffline Mode Ready (All Downloads Settled):'); + console.log( + ` Total: ${(measurements.offlineModeReady.totalBytes / 1024 / 1024).toFixed(2)} MB` + ); + console.log(` Files: ${measurements.offlineModeReady.fileCount}`); + console.log(` Time: ${measurements.offlineModeReady.timestamp}ms`); + + console.log('\nReport written to: bundle-size-report.json'); + } catch (error) { + console.error('Error during measurement:', error); + throw error; + } finally { + await browser.close(); + } + + return measurements; +} + +// Run the measurement +measureBundleSize().catch((error) => { + console.error('Failed to measure bundle size:', error); + process.exit(1); +});