/** * LiveComponents Performance Benchmarks * * Measures and compares performance characteristics of: * - Fragment-based rendering vs Full HTML rendering * - Single vs Multiple fragment updates * - Small vs Large component updates * - Network payload sizes * - DOM update times * * Run with: npx playwright test performance-benchmarks.spec.js * * Generate report: node tests/e2e/livecomponents/generate-performance-report.js */ import { test, expect } from '@playwright/test'; // Performance thresholds const THRESHOLDS = { fragmentRender: { small: 50, // ms for single small fragment medium: 100, // ms for 5-10 fragments large: 200 // ms for complex component }, fullRender: { small: 150, // ms for full render (small component) medium: 300, // ms for full render (medium component) large: 500 // ms for full render (large component) }, networkPayload: { fragmentMax: 5000, // bytes for fragment response fullMax: 50000 // bytes for full HTML response } }; // Store benchmark results for report generation const benchmarkResults = []; /** * Helper: Measure action execution time */ async function measureActionTime(page, componentId, action, params = {}, options = {}) { const startTime = await page.evaluate(() => performance.now()); await page.evaluate(({ id, actionName, actionParams, opts }) => { const component = window.LiveComponents.get(id); return component.call(actionName, actionParams, opts); }, { id: componentId, actionName: action, actionParams: params, opts: options }); // Wait for update to complete await page.waitForTimeout(50); const endTime = await page.evaluate(() => performance.now()); return endTime - startTime; } /** * Helper: Measure network payload size */ async function measurePayloadSize(page, action) { let payloadSize = 0; const responsePromise = page.waitForResponse( response => response.url().includes('/live-component/'), { timeout: 5000 } ); await action(); const response = await responsePromise; const body = await response.text(); payloadSize = new TextEncoder().encode(body).length; return payloadSize; } /** * Helper: Store benchmark result */ function storeBenchmarkResult(scenario, metric, value, threshold, unit = 'ms') { const passed = value <= threshold; benchmarkResults.push({ scenario, metric, value, threshold, unit, passed, timestamp: new Date().toISOString() }); } test.describe('Performance Benchmarks: Fragment vs Full Render', () => { test.beforeEach(async ({ page }) => { await page.goto('https://localhost/livecomponents/test/performance'); await page.waitForFunction(() => window.LiveComponents !== undefined); }); test('Benchmark: Single small fragment update', async ({ page }) => { // Measure fragment update const fragmentTime = await measureActionTime( page, 'counter:benchmark', 'increment', {}, { fragments: ['#counter-value'] } ); // Measure full render const fullTime = await measureActionTime( page, 'counter:benchmark', 'increment', {} ); // Store results storeBenchmarkResult('Single Small Fragment', 'Fragment Update Time', fragmentTime, THRESHOLDS.fragmentRender.small); storeBenchmarkResult('Single Small Fragment', 'Full Render Time', fullTime, THRESHOLDS.fullRender.small); // Assertions expect(fragmentTime).toBeLessThan(THRESHOLDS.fragmentRender.small); expect(fragmentTime).toBeLessThan(fullTime); // Fragment should be faster const speedup = ((fullTime - fragmentTime) / fullTime * 100).toFixed(1); console.log(`Fragment speedup: ${speedup}% faster than full render`); }); test('Benchmark: Multiple fragment updates (5 fragments)', async ({ page }) => { const fragments = [ '#item-1', '#item-2', '#item-3', '#item-4', '#item-5' ]; // Measure fragment update const fragmentTime = await measureActionTime( page, 'list:benchmark', 'updateItems', { count: 5 }, { fragments } ); // Measure full render const fullTime = await measureActionTime( page, 'list:benchmark', 'updateItems', { count: 5 } ); // Store results storeBenchmarkResult('Multiple Fragments (5)', 'Fragment Update Time', fragmentTime, THRESHOLDS.fragmentRender.medium); storeBenchmarkResult('Multiple Fragments (5)', 'Full Render Time', fullTime, THRESHOLDS.fullRender.medium); // Assertions expect(fragmentTime).toBeLessThan(THRESHOLDS.fragmentRender.medium); expect(fragmentTime).toBeLessThan(fullTime); const speedup = ((fullTime - fragmentTime) / fullTime * 100).toFixed(1); console.log(`5-fragment speedup: ${speedup}% faster than full render`); }); test('Benchmark: Large component update (100 items)', async ({ page }) => { const fragments = ['#item-list']; // Measure fragment update const fragmentTime = await measureActionTime( page, 'product-list:benchmark', 'loadItems', { count: 100 }, { fragments } ); // Measure full render const fullTime = await measureActionTime( page, 'product-list:benchmark', 'loadItems', { count: 100 } ); // Store results storeBenchmarkResult('Large Component (100 items)', 'Fragment Update Time', fragmentTime, THRESHOLDS.fragmentRender.large); storeBenchmarkResult('Large Component (100 items)', 'Full Render Time', fullTime, THRESHOLDS.fullRender.large); // Assertions expect(fragmentTime).toBeLessThan(THRESHOLDS.fragmentRender.large); expect(fragmentTime).toBeLessThan(fullTime); const speedup = ((fullTime - fragmentTime) / fullTime * 100).toFixed(1); console.log(`100-item speedup: ${speedup}% faster than full render`); }); test('Benchmark: Network payload size comparison', async ({ page }) => { // Measure fragment payload const fragmentPayload = await measurePayloadSize(page, async () => { await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment', {}, { fragments: ['#counter-value'] }); }); }); // Reset state await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('reset'); }); await page.waitForTimeout(100); // Measure full HTML payload const fullPayload = await measurePayloadSize(page, async () => { await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment'); }); }); // Store results storeBenchmarkResult('Network Payload', 'Fragment Payload Size', fragmentPayload, THRESHOLDS.networkPayload.fragmentMax, 'bytes'); storeBenchmarkResult('Network Payload', 'Full HTML Payload Size', fullPayload, THRESHOLDS.networkPayload.fullMax, 'bytes'); // Assertions expect(fragmentPayload).toBeLessThan(THRESHOLDS.networkPayload.fragmentMax); expect(fullPayload).toBeLessThan(THRESHOLDS.networkPayload.fullMax); expect(fragmentPayload).toBeLessThan(fullPayload); const reduction = ((fullPayload - fragmentPayload) / fullPayload * 100).toFixed(1); console.log(`Fragment payload reduction: ${reduction}% smaller than full HTML`); console.log(`Fragment: ${fragmentPayload} bytes, Full: ${fullPayload} bytes`); }); test('Benchmark: Rapid successive updates (10 updates)', async ({ page }) => { const updateCount = 10; // Measure fragment updates const fragmentStartTime = await page.evaluate(() => performance.now()); for (let i = 0; i < updateCount; i++) { await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment', {}, { fragments: ['#counter-value'] }); }); await page.waitForTimeout(10); // Small delay between updates } const fragmentEndTime = await page.evaluate(() => performance.now()); const fragmentTotalTime = fragmentEndTime - fragmentStartTime; // Reset await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('reset'); }); await page.waitForTimeout(100); // Measure full renders const fullStartTime = await page.evaluate(() => performance.now()); for (let i = 0; i < updateCount; i++) { await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment'); }); await page.waitForTimeout(10); } const fullEndTime = await page.evaluate(() => performance.now()); const fullTotalTime = fullEndTime - fullStartTime; // Store results storeBenchmarkResult('Rapid Updates (10x)', 'Fragment Total Time', fragmentTotalTime, 500); storeBenchmarkResult('Rapid Updates (10x)', 'Full Render Total Time', fullTotalTime, 1500); // Assertions expect(fragmentTotalTime).toBeLessThan(fullTotalTime); const avgFragment = fragmentTotalTime / updateCount; const avgFull = fullTotalTime / updateCount; console.log(`Average fragment update: ${avgFragment.toFixed(2)}ms`); console.log(`Average full render: ${avgFull.toFixed(2)}ms`); console.log(`Fragment ${((fullTotalTime / fragmentTotalTime).toFixed(1))}x faster for rapid updates`); }); test('Benchmark: DOM manipulation overhead', async ({ page }) => { // Measure pure DOM update time (client-side only) const domUpdateTime = await page.evaluate(() => { const container = document.querySelector('[data-component-id="counter:benchmark"]'); const fragment = container.querySelector('[data-lc-fragment="counter-value"]'); const startTime = performance.now(); // Simulate fragment update fragment.textContent = 'Updated Value'; const endTime = performance.now(); return endTime - startTime; }); // Measure network + server time for fragment update const startTime = await page.evaluate(() => performance.now()); await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment', {}, { fragments: ['#counter-value'] }); }); await page.waitForTimeout(50); const endTime = await page.evaluate(() => performance.now()); const totalTime = endTime - startTime; const networkServerTime = totalTime - domUpdateTime; // Store results storeBenchmarkResult('DOM Overhead', 'Pure DOM Update', domUpdateTime, 5); storeBenchmarkResult('DOM Overhead', 'Network + Server Time', networkServerTime, 100); storeBenchmarkResult('DOM Overhead', 'Total Fragment Update', totalTime, 150); console.log(`DOM update: ${domUpdateTime.toFixed(2)}ms`); console.log(`Network + Server: ${networkServerTime.toFixed(2)}ms`); console.log(`Total: ${totalTime.toFixed(2)}ms`); expect(domUpdateTime).toBeLessThan(5); // DOM should be very fast }); test('Benchmark: Memory consumption comparison', async ({ page }) => { // Measure memory before const memoryBefore = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize; } return 0; }); // Perform 50 fragment updates for (let i = 0; i < 50; i++) { await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment', {}, { fragments: ['#counter-value'] }); }); await page.waitForTimeout(10); } // Measure memory after const memoryAfterFragments = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize; } return 0; }); // Reset await page.reload(); await page.waitForFunction(() => window.LiveComponents !== undefined); const memoryBeforeFull = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize; } return 0; }); // Perform 50 full renders for (let i = 0; i < 50; i++) { await page.evaluate(() => { window.LiveComponents.get('counter:benchmark').call('increment'); }); await page.waitForTimeout(10); } const memoryAfterFull = await page.evaluate(() => { if (performance.memory) { return performance.memory.usedJSHeapSize; } return 0; }); if (memoryBefore > 0) { const fragmentMemoryDelta = memoryAfterFragments - memoryBefore; const fullMemoryDelta = memoryAfterFull - memoryBeforeFull; storeBenchmarkResult('Memory Consumption (50 updates)', 'Fragment Updates Memory Delta', fragmentMemoryDelta, 1000000, 'bytes'); storeBenchmarkResult('Memory Consumption (50 updates)', 'Full Renders Memory Delta', fullMemoryDelta, 2000000, 'bytes'); console.log(`Fragment memory delta: ${(fragmentMemoryDelta / 1024).toFixed(2)} KB`); console.log(`Full render memory delta: ${(fullMemoryDelta / 1024).toFixed(2)} KB`); } else { console.log('Memory API not available (Firefox/Safari)'); } }); test('Benchmark: Cache effectiveness', async ({ page }) => { // First update (no cache) const firstUpdateTime = await measureActionTime( page, 'counter:benchmark', 'increment', {}, { fragments: ['#counter-value'] } ); // Second update (potentially cached) const secondUpdateTime = await measureActionTime( page, 'counter:benchmark', 'increment', {}, { fragments: ['#counter-value'] } ); // Third update (cached) const thirdUpdateTime = await measureActionTime( page, 'counter:benchmark', 'increment', {}, { fragments: ['#counter-value'] } ); const avgCachedTime = (secondUpdateTime + thirdUpdateTime) / 2; storeBenchmarkResult('Cache Effectiveness', 'First Update (Cold)', firstUpdateTime, 100); storeBenchmarkResult('Cache Effectiveness', 'Average Cached Update', avgCachedTime, 80); console.log(`First update (cold): ${firstUpdateTime.toFixed(2)}ms`); console.log(`Average cached: ${avgCachedTime.toFixed(2)}ms`); if (avgCachedTime < firstUpdateTime) { const improvement = ((firstUpdateTime - avgCachedTime) / firstUpdateTime * 100).toFixed(1); console.log(`Cache improves performance by ${improvement}%`); } }); // After all tests, write results to file for report generation test.afterAll(async () => { const fs = await import('fs'); const path = await import('path'); const resultsFile = path.default.join( process.cwd(), 'test-results', 'benchmark-results.json' ); // Ensure directory exists const dir = path.default.dirname(resultsFile); if (!fs.default.existsSync(dir)) { fs.default.mkdirSync(dir, { recursive: true }); } fs.default.writeFileSync( resultsFile, JSON.stringify({ timestamp: new Date().toISOString(), results: benchmarkResults, summary: { total: benchmarkResults.length, passed: benchmarkResults.filter(r => r.passed).length, failed: benchmarkResults.filter(r => !r.passed).length } }, null, 2) ); console.log(`\nBenchmark results written to: ${resultsFile}`); console.log(`Total benchmarks: ${benchmarkResults.length}`); console.log(`Passed: ${benchmarkResults.filter(r => r.passed).length}`); console.log(`Failed: ${benchmarkResults.filter(r => !r.passed).length}`); }); });