find-bottleneck-perf 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Metric Extractor
3
+ *
4
+ * Uses Puppeteer to load a URL and extract comprehensive performance metrics
5
+ * from the browser's Performance API and network activity.
6
+ */
7
+
8
+ const puppeteer = require('puppeteer');
9
+
10
+ /**
11
+ * Extract all performance metrics from a URL.
12
+ * @param {string} url - URL to analyze
13
+ * @param {object} options - Configuration options
14
+ * @param {object} options.networkCondition - Network throttling preset
15
+ * @returns {Promise<object>} Extracted metrics
16
+ */
17
+ async function extractMetrics(url, options = {}) {
18
+ const {
19
+ timeout = 60000,
20
+ waitUntil = 'networkidle2',
21
+ viewport = { width: 1920, height: 1080 },
22
+ networkCondition = null, // Network throttling preset
23
+ } = options;
24
+
25
+ let browser;
26
+
27
+ try {
28
+ browser = await puppeteer.launch({
29
+ headless: 'new',
30
+ args: [
31
+ '--no-sandbox',
32
+ '--disable-setuid-sandbox',
33
+ '--disable-blink-features=AutomationControlled', // Hide automation
34
+ '--disable-http2', // Avoid HTTP2 protocol errors
35
+ '--disable-features=IsolateOrigins,site-per-process',
36
+ '--disable-web-security',
37
+ '--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
38
+ ],
39
+ });
40
+
41
+ const page = await browser.newPage();
42
+ await page.setViewport(viewport);
43
+
44
+ // === Apply Network Throttling via CDP ===
45
+ if (networkCondition) {
46
+ const client = await page.target().createCDPSession();
47
+ await client.send('Network.enable');
48
+ await client.send('Network.emulateNetworkConditions', {
49
+ offline: networkCondition.offline || false,
50
+ downloadThroughput: networkCondition.downloadThroughput,
51
+ uploadThroughput: networkCondition.uploadThroughput,
52
+ latency: networkCondition.latency,
53
+ });
54
+ }
55
+
56
+ // === Network Request Tracking ===
57
+ const resources = {
58
+ scripts: [], // JS files
59
+ stylesheets: [], // CSS files
60
+ images: [], // Images
61
+ fonts: [], // Font files
62
+ apis: [], // XHR/Fetch requests
63
+ other: [], // Everything else
64
+ };
65
+
66
+ let failedRequests = 0;
67
+
68
+ await page.setRequestInterception(true);
69
+ page.on('request', (req) => req.continue());
70
+
71
+ page.on('response', async (response) => {
72
+ const reqUrl = response.url();
73
+ const resourceType = response.request().resourceType();
74
+ const status = response.status();
75
+
76
+ // Track failed requests
77
+ if (status >= 400) {
78
+ failedRequests++;
79
+ }
80
+
81
+ // Get response size (safely)
82
+ let size = 0;
83
+ try {
84
+ const buffer = await response.buffer();
85
+ size = buffer.length;
86
+ } catch (e) {
87
+ // Response body unavailable (e.g., redirects)
88
+ }
89
+
90
+ const timing = response.timing();
91
+ const duration = timing ? timing.receiveHeadersEnd : 0;
92
+
93
+ const entry = { url: reqUrl, size, duration, status };
94
+
95
+ switch (resourceType) {
96
+ case 'script':
97
+ resources.scripts.push(entry);
98
+ break;
99
+ case 'stylesheet':
100
+ resources.stylesheets.push(entry);
101
+ break;
102
+ case 'image':
103
+ resources.images.push(entry);
104
+ break;
105
+ case 'font':
106
+ resources.fonts.push(entry);
107
+ break;
108
+ case 'xhr':
109
+ case 'fetch':
110
+ resources.apis.push(entry);
111
+ break;
112
+ default:
113
+ resources.other.push(entry);
114
+ }
115
+ });
116
+
117
+ // === Page Load ===
118
+ const loadStart = Date.now();
119
+ await page.goto(url, { waitUntil, timeout });
120
+ const totalLoadTime = Date.now() - loadStart;
121
+
122
+ // === Extract Navigation Timing ===
123
+ const navTiming = await page.evaluate(() => {
124
+ const nav = performance.getEntriesByType('navigation')[0];
125
+ if (!nav) return null;
126
+
127
+ return {
128
+ // Connection phases
129
+ dnsLookup: nav.domainLookupEnd - nav.domainLookupStart,
130
+ tcpConnection: nav.connectEnd - nav.connectStart,
131
+ tlsHandshake: nav.secureConnectionStart > 0
132
+ ? nav.connectEnd - nav.secureConnectionStart
133
+ : 0,
134
+
135
+ // Request/Response
136
+ ttfb: nav.responseStart - nav.requestStart,
137
+ responseTime: nav.responseEnd - nav.responseStart,
138
+
139
+ // Document Processing
140
+ domInteractive: nav.domInteractive,
141
+ domContentLoaded: nav.domContentLoadedEventEnd,
142
+ domComplete: nav.domComplete,
143
+ loadEvent: nav.loadEventEnd,
144
+
145
+ // Derived: Server processing time (TTFB minus network overhead)
146
+ serverProcessing: nav.responseStart - nav.connectEnd,
147
+ };
148
+ });
149
+
150
+ // === Extract Paint Timing ===
151
+ const paintTiming = await page.evaluate(() => {
152
+ const entries = performance.getEntriesByType('paint');
153
+ const fcp = entries.find(e => e.name === 'first-contentful-paint');
154
+ const fp = entries.find(e => e.name === 'first-paint');
155
+
156
+ return {
157
+ firstPaint: fp ? fp.startTime : 0,
158
+ firstContentfulPaint: fcp ? fcp.startTime : 0,
159
+ };
160
+ });
161
+
162
+ // === Extract LCP ===
163
+ const lcp = await page.evaluate(() => {
164
+ return new Promise((resolve) => {
165
+ let lcpValue = 0;
166
+
167
+ try {
168
+ new PerformanceObserver((entryList) => {
169
+ const entries = entryList.getEntries();
170
+ lcpValue = entries[entries.length - 1]?.startTime || 0;
171
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
172
+ } catch (e) {
173
+ // LCP not supported
174
+ }
175
+
176
+ // Wait for LCP to stabilize
177
+ setTimeout(() => resolve(lcpValue), 2000);
178
+ });
179
+ });
180
+
181
+ // === Extract Long Tasks (Total Blocking Time) ===
182
+ const longTasks = await page.evaluate(() => {
183
+ return new Promise((resolve) => {
184
+ let totalBlocking = 0;
185
+ let taskCount = 0;
186
+
187
+ try {
188
+ new PerformanceObserver((list) => {
189
+ for (const entry of list.getEntries()) {
190
+ // Long task is > 50ms, blocking time is the excess
191
+ const blocking = entry.duration - 50;
192
+ totalBlocking += blocking > 0 ? blocking : 0;
193
+ taskCount++;
194
+ }
195
+ }).observe({ type: 'longtask', buffered: true });
196
+ } catch (e) {
197
+ // Long tasks not supported
198
+ }
199
+
200
+ setTimeout(() => resolve({ totalBlocking, taskCount }), 1500);
201
+ });
202
+ });
203
+
204
+ // === Extract Resource Hints & Render Blocking ===
205
+ const renderBlocking = await page.evaluate(() => {
206
+ // Count render-blocking resources (sync scripts in head, blocking stylesheets)
207
+ const blockingScripts = document.querySelectorAll('head script:not([async]):not([defer])').length;
208
+ const blockingStyles = document.querySelectorAll('link[rel="stylesheet"]').length;
209
+
210
+ return {
211
+ blockingScripts,
212
+ blockingStyles,
213
+ total: blockingScripts + blockingStyles,
214
+ };
215
+ });
216
+
217
+ await browser.close();
218
+
219
+ // === Aggregate Resource Sizes ===
220
+ const sumSize = (arr) => arr.reduce((sum, r) => sum + r.size, 0);
221
+ const avgDuration = (arr) => arr.length > 0
222
+ ? arr.reduce((sum, r) => sum + r.duration, 0) / arr.length
223
+ : 0;
224
+ const maxDuration = (arr) => arr.length > 0
225
+ ? Math.max(...arr.map(r => r.duration))
226
+ : 0;
227
+
228
+ // === Compile Final Metrics ===
229
+ return {
230
+ url,
231
+ timestamp: new Date().toISOString(),
232
+
233
+ // Navigation Timing
234
+ navigation: {
235
+ dnsLookup: Math.round(navTiming?.dnsLookup || 0),
236
+ tcpConnection: Math.round(navTiming?.tcpConnection || 0),
237
+ tlsHandshake: Math.round(navTiming?.tlsHandshake || 0),
238
+ ttfb: Math.round(navTiming?.ttfb || 0),
239
+ responseTime: Math.round(navTiming?.responseTime || 0),
240
+ serverProcessing: Math.round(navTiming?.serverProcessing || 0),
241
+ domInteractive: Math.round(navTiming?.domInteractive || 0),
242
+ domContentLoaded: Math.round(navTiming?.domContentLoaded || 0),
243
+ domComplete: Math.round(navTiming?.domComplete || 0),
244
+ totalLoadTime,
245
+ },
246
+
247
+ // Paint & Vitals
248
+ vitals: {
249
+ firstPaint: Math.round(paintTiming.firstPaint),
250
+ firstContentfulPaint: Math.round(paintTiming.firstContentfulPaint),
251
+ largestContentfulPaint: Math.round(lcp),
252
+ totalBlockingTime: Math.round(longTasks.totalBlocking),
253
+ longTaskCount: longTasks.taskCount,
254
+ },
255
+
256
+ // Resource Breakdown
257
+ resources: {
258
+ javascript: {
259
+ count: resources.scripts.length,
260
+ totalBytes: sumSize(resources.scripts),
261
+ },
262
+ css: {
263
+ count: resources.stylesheets.length,
264
+ totalBytes: sumSize(resources.stylesheets),
265
+ },
266
+ images: {
267
+ count: resources.images.length,
268
+ totalBytes: sumSize(resources.images),
269
+ },
270
+ fonts: {
271
+ count: resources.fonts.length,
272
+ totalBytes: sumSize(resources.fonts),
273
+ },
274
+ other: {
275
+ count: resources.other.length,
276
+ totalBytes: sumSize(resources.other),
277
+ },
278
+ },
279
+
280
+ // API Performance
281
+ api: {
282
+ callCount: resources.apis.length,
283
+ avgLatency: Math.round(avgDuration(resources.apis)),
284
+ maxLatency: Math.round(maxDuration(resources.apis)),
285
+ failedCount: resources.apis.filter(r => r.status >= 400).length,
286
+ totalFailedRequests: failedRequests,
287
+ },
288
+
289
+ // Render Blocking
290
+ renderBlocking,
291
+
292
+ // Derived Metrics (calculated in analyzer)
293
+ derived: null,
294
+ };
295
+
296
+ } catch (err) {
297
+ if (browser) await browser.close();
298
+ throw err;
299
+ }
300
+ }
301
+
302
+ module.exports = { extractMetrics };
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Performance Reporter
3
+ *
4
+ * Formats analysis results for terminal output with colors,
5
+ * tables, and clear visual hierarchy.
6
+ */
7
+
8
+ const { colors, colorize, formatMs, formatBytes, getStatus, progressBar } = require('./utils');
9
+ const T = require('./thresholds');
10
+
11
+ /**
12
+ * Print the full analysis report to terminal.
13
+ * @param {object} analysis - Analysis result from analyzer
14
+ */
15
+ function printReport(analysis) {
16
+ const { verdict, scores, evidence, metrics } = analysis;
17
+ const nav = metrics.navigation;
18
+ const vitals = metrics.vitals;
19
+ const derived = metrics.derived;
20
+ const api = metrics.api;
21
+ const res = metrics.resources;
22
+
23
+ console.log('\n' + '═'.repeat(60));
24
+ console.log(colorize(' PERF-BLAME-FINDER │ Performance Analysis Report', 'cyan'));
25
+ console.log('═'.repeat(60));
26
+ console.log(colorize(` URL: ${metrics.url}`, 'dim'));
27
+ console.log(colorize(` Analyzed: ${metrics.timestamp}`, 'dim'));
28
+ console.log('─'.repeat(60));
29
+
30
+ // === VERDICT ===
31
+ printVerdict(verdict, scores);
32
+
33
+ // === METRICS SUMMARY ===
34
+ printMetricsSummary(nav, vitals, derived, api, res);
35
+
36
+ // === EVIDENCE ===
37
+ printEvidence(evidence);
38
+
39
+ // === RECOMMENDATIONS ===
40
+ const { getRecommendations } = require('./analyzer');
41
+ const recs = getRecommendations(verdict, evidence);
42
+ printRecommendations(recs);
43
+
44
+ console.log('═'.repeat(60) + '\n');
45
+ }
46
+
47
+ function printVerdict(verdict, scores) {
48
+ console.log('\n' + colorize('┌─ THE VERDICT', 'bright'));
49
+ console.log('│');
50
+
51
+ let verdictColor, verdictIcon;
52
+ switch (verdict) {
53
+ case 'Optimized':
54
+ verdictColor = 'green';
55
+ verdictIcon = '✅';
56
+ break;
57
+ case 'Frontend':
58
+ verdictColor = 'blue';
59
+ verdictIcon = '🖥️';
60
+ break;
61
+ case 'Backend':
62
+ verdictColor = 'yellow';
63
+ verdictIcon = '⚙️';
64
+ break;
65
+ case 'Infra':
66
+ verdictColor = 'red';
67
+ verdictIcon = '🌐';
68
+ break;
69
+ case 'Mixed':
70
+ verdictColor = 'magenta';
71
+ verdictIcon = '⚠️';
72
+ break;
73
+ default:
74
+ verdictColor = 'white';
75
+ verdictIcon = '❓';
76
+ }
77
+
78
+ console.log(`│ ${verdictIcon} Primary Bottleneck: ${colorize(verdict.toUpperCase(), verdictColor)}`);
79
+ console.log('│');
80
+ console.log(`│ Scores: Frontend=${scores.Frontend} Backend=${scores.Backend} Infra=${scores.Infra}`);
81
+ console.log('└' + '─'.repeat(59));
82
+ }
83
+
84
+ function printMetricsSummary(nav, vitals, derived, api, res) {
85
+ console.log('\n' + colorize('┌─ METRICS SUMMARY', 'bright'));
86
+
87
+ // Navigation Timing
88
+ console.log('│');
89
+ console.log('│ ' + colorize('Navigation Timing', 'cyan'));
90
+ printMetricRow(' DNS Lookup', nav.dnsLookup, 'ms', T.DNS);
91
+ printMetricRow(' TCP Connect', nav.tcpConnection, 'ms', T.TCP);
92
+ printMetricRow(' TLS Handshake', nav.tlsHandshake, 'ms', T.TLS);
93
+ printMetricRow(' TTFB', nav.ttfb, 'ms', T.TTFB);
94
+ printMetricRow(' Server Processing', nav.serverProcessing, 'ms', T.TTFB);
95
+ printMetricRow(' DOM Content Loaded', nav.domContentLoaded, 'ms');
96
+ printMetricRow(' Total Load Time', nav.totalLoadTime, 'ms');
97
+
98
+ // Core Web Vitals
99
+ console.log('│');
100
+ console.log('│ ' + colorize('Core Web Vitals', 'cyan'));
101
+ printMetricRow(' First Contentful Paint', vitals.firstContentfulPaint, 'ms', T.FCP);
102
+ printMetricRow(' Largest Contentful Paint', vitals.largestContentfulPaint, 'ms', T.LCP);
103
+ printMetricRow(' Total Blocking Time', vitals.totalBlockingTime, 'ms', T.JS_EXECUTION);
104
+ printMetricRow(' Long Task Count', vitals.longTaskCount);
105
+
106
+ // Resources
107
+ console.log('│');
108
+ console.log('│ ' + colorize('Resource Sizes', 'cyan'));
109
+ printMetricRow(' JavaScript', derived.jsSizeMB, 'MB', T.BUNDLE_SIZE_MB);
110
+ printMetricRow(' CSS', derived.cssSizeMB, 'MB', T.CSS_SIZE_MB);
111
+ printMetricRow(' Images', derived.imageSizeMB, 'MB', T.IMAGE_SIZE_MB);
112
+ printMetricRow(' Total', derived.totalSizeMB, 'MB');
113
+
114
+ // API
115
+ console.log('│');
116
+ console.log('│ ' + colorize('API Performance', 'cyan'));
117
+ printMetricRow(' Request Count', api.callCount, '', T.API_CALLS);
118
+ printMetricRow(' Avg Latency', api.avgLatency, 'ms', T.API_LATENCY);
119
+ printMetricRow(' Max Latency', api.maxLatency, 'ms');
120
+ printMetricRow(' Failed Requests', api.failedCount);
121
+
122
+ // Derived
123
+ console.log('│');
124
+ console.log('│ ' + colorize('Derived Metrics', 'cyan'));
125
+ printMetricRow(' Network Overhead', derived.networkOverhead, 'ms');
126
+ printMetricRow(' Network Overhead Ratio', (derived.networkOverheadRatio * 100).toFixed(0), '%');
127
+ printMetricRow(' Client Render Time', derived.clientRenderTime, 'ms', T.CLIENT_RENDER_TIME);
128
+
129
+ console.log('└' + '─'.repeat(59));
130
+ }
131
+
132
+ function printMetricRow(label, value, unit = '', threshold = null) {
133
+ const formattedValue = typeof value === 'number'
134
+ ? (unit === 'MB' ? value.toFixed(2) : Math.round(value))
135
+ : value;
136
+
137
+ let status = ' ';
138
+ if (threshold) {
139
+ // Determine status based on threshold
140
+ if (threshold.GOOD !== undefined && threshold.POOR !== undefined) {
141
+ if (value <= threshold.GOOD) status = colorize('●', 'green');
142
+ else if (value >= threshold.POOR) status = colorize('●', 'red');
143
+ else status = colorize('●', 'yellow');
144
+ } else if (threshold.NORMAL !== undefined) {
145
+ // For counts like API calls
146
+ if (value <= threshold.NORMAL) status = colorize('●', 'green');
147
+ else if (value >= threshold.EXCESSIVE) status = colorize('●', 'red');
148
+ else status = colorize('●', 'yellow');
149
+ }
150
+ }
151
+
152
+ const valueStr = `${formattedValue}${unit}`;
153
+ console.log(`│ ${status} ${label.padEnd(25)} ${valueStr.padStart(10)}`);
154
+ }
155
+
156
+ function printEvidence(evidence) {
157
+ console.log('\n' + colorize('┌─ EVIDENCE', 'bright'));
158
+
159
+ if (evidence.length === 0) {
160
+ console.log('│ No significant issues detected.');
161
+ } else {
162
+ // Group by category
163
+ const grouped = {};
164
+ for (const e of evidence) {
165
+ if (!grouped[e.category]) grouped[e.category] = [];
166
+ grouped[e.category].push(e);
167
+ }
168
+
169
+ for (const [category, items] of Object.entries(grouped)) {
170
+ console.log('│');
171
+ const catColor = category === 'Frontend' ? 'blue'
172
+ : category === 'Backend' ? 'yellow'
173
+ : 'red';
174
+ console.log('│ ' + colorize(`[${category}]`, catColor));
175
+
176
+ for (const item of items) {
177
+ const severityIcon = item.severity === 'high' ? colorize('▸', 'red') : colorize('▸', 'yellow');
178
+ console.log(`│ ${severityIcon} ${item.message}`);
179
+ }
180
+ }
181
+ }
182
+
183
+ console.log('└' + '─'.repeat(59));
184
+ }
185
+
186
+ function printRecommendations(recs) {
187
+ console.log('\n' + colorize('┌─ RECOMMENDATIONS', 'bright'));
188
+ console.log('│');
189
+
190
+ for (const rec of recs) {
191
+ console.log(`│ 👉 ${rec}`);
192
+ }
193
+
194
+ console.log('└' + '─'.repeat(59));
195
+ }
196
+
197
+ /**
198
+ * Export full analysis as JSON.
199
+ * @param {object} analysis - Full analysis result
200
+ * @param {string} filepath - Output file path
201
+ */
202
+ function exportJson(analysis, filepath) {
203
+ const fs = require('fs');
204
+ const { getRecommendations } = require('./analyzer');
205
+
206
+ const { verdict, scores, evidence, metrics } = analysis;
207
+ const recommendations = getRecommendations(verdict, evidence);
208
+
209
+ // Build clean export object with all data
210
+ const exportData = {
211
+ url: metrics.url,
212
+ timestamp: metrics.timestamp,
213
+
214
+ // The verdict
215
+ verdict: {
216
+ primary: verdict,
217
+ scores: scores,
218
+ },
219
+
220
+ // All metrics
221
+ metrics: {
222
+ navigation: metrics.navigation,
223
+ vitals: metrics.vitals,
224
+ resources: metrics.resources,
225
+ api: metrics.api,
226
+ renderBlocking: metrics.renderBlocking,
227
+ derived: metrics.derived,
228
+ },
229
+
230
+ // Evidence (cleaned for JSON)
231
+ evidence: evidence.map(e => ({
232
+ category: e.category,
233
+ severity: e.severity,
234
+ metric: e.metric,
235
+ value: e.value,
236
+ threshold: e.threshold,
237
+ message: e.message,
238
+ })),
239
+
240
+ // Recommendations
241
+ recommendations: recommendations,
242
+ };
243
+
244
+ fs.writeFileSync(filepath, JSON.stringify(exportData, null, 2));
245
+ console.log(colorize(`\n📁 Full report exported to: ${filepath}`, 'green'));
246
+ }
247
+
248
+ module.exports = { printReport, exportJson };
249
+
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Threshold configuration for performance metrics.
3
+ * All time values are in milliseconds, sizes in MB.
4
+ *
5
+ * These are opinionated, production-realistic values based on:
6
+ * - Google's Core Web Vitals guidelines
7
+ * - Real-world SRE experience
8
+ * - Perceived performance research
9
+ */
10
+
11
+ module.exports = {
12
+ // === Navigation Timing ===
13
+ TTFB: {
14
+ GOOD: 200, // Fast server, edge-cached
15
+ MODERATE: 400, // Typical origin response
16
+ POOR: 600, // Noticeable delay
17
+ },
18
+
19
+ DNS: {
20
+ GOOD: 20, // Cached or fast resolver
21
+ POOR: 100, // Slow DNS or cold lookup
22
+ },
23
+
24
+ TCP: {
25
+ GOOD: 50, // Low-latency connection
26
+ POOR: 200, // High-latency or congested
27
+ },
28
+
29
+ TLS: {
30
+ GOOD: 50, // Session resumption
31
+ POOR: 150, // Full handshake
32
+ },
33
+
34
+ // === Core Web Vitals ===
35
+ LCP: {
36
+ GOOD: 2500, // Google's "good" threshold
37
+ POOR: 4000, // Google's "poor" threshold
38
+ },
39
+
40
+ FCP: {
41
+ GOOD: 1800,
42
+ POOR: 3000,
43
+ },
44
+
45
+ // === JavaScript ===
46
+ JS_EXECUTION: {
47
+ GOOD: 200, // Minimal blocking
48
+ MODERATE: 500, // Noticeable but acceptable
49
+ POOR: 800, // Heavy blocking
50
+ },
51
+
52
+ BUNDLE_SIZE_MB: {
53
+ GOOD: 0.5, // Well-optimized
54
+ MODERATE: 1.0, // Typical SPA
55
+ POOR: 2.0, // Bloated
56
+ },
57
+
58
+ // === API Performance ===
59
+ API_LATENCY: {
60
+ GOOD: 100, // Snappy API
61
+ MODERATE: 200, // Acceptable
62
+ POOR: 300, // Sluggish
63
+ },
64
+
65
+ API_CALLS: {
66
+ NORMAL: 15, // Reasonable for initial load
67
+ HIGH: 30, // Chatty
68
+ EXCESSIVE: 50, // Problematic
69
+ },
70
+
71
+ // === Resource Sizes (MB) ===
72
+ CSS_SIZE_MB: {
73
+ GOOD: 0.1,
74
+ POOR: 0.3,
75
+ },
76
+
77
+ IMAGE_SIZE_MB: {
78
+ GOOD: 1.0,
79
+ POOR: 3.0,
80
+ },
81
+
82
+ // === Derived Ratios ===
83
+ NETWORK_OVERHEAD_RATIO: {
84
+ HIGH: 0.5, // > 50% of TTFB is network overhead
85
+ },
86
+
87
+ CLIENT_RENDER_TIME: {
88
+ GOOD: 1500, // Fast render after TTFB
89
+ POOR: 2500, // Slow client-side processing
90
+ },
91
+ };
92
+
93
+ /**
94
+ * Network Throttling Presets
95
+ * Based on Chrome DevTools network conditions
96
+ *
97
+ * downloadThroughput/uploadThroughput: bytes per second
98
+ * latency: additional RTT in ms
99
+ */
100
+ module.exports.NETWORK_PRESETS = {
101
+ 'no-throttle': {
102
+ name: 'No Throttle',
103
+ downloadThroughput: -1, // Unlimited
104
+ uploadThroughput: -1,
105
+ latency: 0,
106
+ },
107
+ 'slow-3g': {
108
+ name: 'Slow 3G',
109
+ downloadThroughput: 500 * 1024 / 8, // 500 Kbps
110
+ uploadThroughput: 500 * 1024 / 8,
111
+ latency: 400, // 400ms RTT
112
+ },
113
+ 'fast-3g': {
114
+ name: 'Fast 3G',
115
+ downloadThroughput: 1.6 * 1024 * 1024 / 8, // 1.6 Mbps
116
+ uploadThroughput: 750 * 1024 / 8,
117
+ latency: 150, // 150ms RTT
118
+ },
119
+ '4g': {
120
+ name: 'Regular 4G',
121
+ downloadThroughput: 4 * 1024 * 1024 / 8, // 4 Mbps
122
+ uploadThroughput: 3 * 1024 * 1024 / 8,
123
+ latency: 20, // 20ms RTT
124
+ },
125
+ 'wifi': {
126
+ name: 'WiFi',
127
+ downloadThroughput: 30 * 1024 * 1024 / 8, // 30 Mbps
128
+ uploadThroughput: 15 * 1024 * 1024 / 8,
129
+ latency: 2, // 2ms RTT
130
+ },
131
+ 'offline': {
132
+ name: 'Offline',
133
+ offline: true,
134
+ downloadThroughput: 0,
135
+ uploadThroughput: 0,
136
+ latency: 0,
137
+ },
138
+ };