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,432 @@
1
+ /**
2
+ * Performance Analyzer
3
+ *
4
+ * Analyzes extracted metrics using heuristics to determine the primary
5
+ * bottleneck and generate actionable evidence.
6
+ */
7
+
8
+ const T = require('./thresholds');
9
+
10
+ /**
11
+ * Analyze metrics and determine bottleneck.
12
+ * @param {object} metrics - Extracted metrics from extractor
13
+ * @returns {object} Analysis result with verdict, evidence, and scores
14
+ */
15
+ function analyze(metrics) {
16
+ const nav = metrics.navigation;
17
+ const vitals = metrics.vitals;
18
+ const res = metrics.resources;
19
+ const api = metrics.api;
20
+
21
+ // === Calculate Derived Metrics ===
22
+ const jsSizeMB = res.javascript.totalBytes / 1024 / 1024;
23
+ const cssSizeMB = res.css.totalBytes / 1024 / 1024;
24
+ const imageSizeMB = res.images.totalBytes / 1024 / 1024;
25
+ const totalSizeMB = jsSizeMB + cssSizeMB + imageSizeMB +
26
+ (res.fonts.totalBytes / 1024 / 1024) + (res.other.totalBytes / 1024 / 1024);
27
+
28
+ const networkOverhead = nav.dnsLookup + nav.tcpConnection + nav.tlsHandshake;
29
+ const networkOverheadRatio = nav.ttfb > 0 ? networkOverhead / nav.ttfb : 0;
30
+ const clientRenderTime = vitals.largestContentfulPaint - nav.ttfb;
31
+
32
+ metrics.derived = {
33
+ jsSizeMB: parseFloat(jsSizeMB.toFixed(2)),
34
+ cssSizeMB: parseFloat(cssSizeMB.toFixed(2)),
35
+ imageSizeMB: parseFloat(imageSizeMB.toFixed(2)),
36
+ totalSizeMB: parseFloat(totalSizeMB.toFixed(2)),
37
+ networkOverhead,
38
+ networkOverheadRatio: parseFloat(networkOverheadRatio.toFixed(2)),
39
+ clientRenderTime: Math.round(clientRenderTime),
40
+ };
41
+
42
+ // === Scoring System ===
43
+ const scores = {
44
+ Frontend: 0,
45
+ Backend: 0,
46
+ Infra: 0,
47
+ };
48
+
49
+ const evidence = [];
50
+
51
+ // =========================================
52
+ // INFRASTRUCTURE / NETWORK ANALYSIS
53
+ // =========================================
54
+
55
+ // DNS Lookup
56
+ if (nav.dnsLookup > T.DNS.POOR) {
57
+ scores.Infra += 5;
58
+ evidence.push({
59
+ category: 'Infra',
60
+ severity: 'high',
61
+ metric: 'DNS Lookup',
62
+ value: nav.dnsLookup,
63
+ threshold: T.DNS.POOR,
64
+ message: `Slow DNS resolution (${nav.dnsLookup}ms). Consider DNS prefetching or faster resolver.`,
65
+ });
66
+ }
67
+
68
+ // TCP Connection
69
+ if (nav.tcpConnection > T.TCP.POOR) {
70
+ scores.Infra += 5;
71
+ evidence.push({
72
+ category: 'Infra',
73
+ severity: 'high',
74
+ metric: 'TCP Connection',
75
+ value: nav.tcpConnection,
76
+ threshold: T.TCP.POOR,
77
+ message: `High TCP latency (${nav.tcpConnection}ms). Server may be geographically distant.`,
78
+ });
79
+ }
80
+
81
+ // TLS Handshake
82
+ if (nav.tlsHandshake > T.TLS.POOR) {
83
+ scores.Infra += 4;
84
+ evidence.push({
85
+ category: 'Infra',
86
+ severity: 'medium',
87
+ metric: 'TLS Handshake',
88
+ value: nav.tlsHandshake,
89
+ threshold: T.TLS.POOR,
90
+ message: `Slow TLS handshake (${nav.tlsHandshake}ms). Consider TLS 1.3 or session resumption.`,
91
+ });
92
+ }
93
+
94
+ // TTFB Analysis (nuanced: check if it's network or backend)
95
+ if (nav.ttfb > T.TTFB.POOR) {
96
+ // If network overhead is high relative to TTFB, it's Infra
97
+ if (networkOverheadRatio > T.NETWORK_OVERHEAD_RATIO.HIGH) {
98
+ scores.Infra += 8;
99
+ evidence.push({
100
+ category: 'Infra',
101
+ severity: 'high',
102
+ metric: 'TTFB (Network)',
103
+ value: nav.ttfb,
104
+ threshold: T.TTFB.POOR,
105
+ message: `High TTFB (${nav.ttfb}ms) dominated by network overhead (${Math.round(networkOverheadRatio * 100)}%).`,
106
+ });
107
+ } else {
108
+ // Server processing is the bottleneck
109
+ scores.Backend += 8;
110
+ evidence.push({
111
+ category: 'Backend',
112
+ severity: 'high',
113
+ metric: 'TTFB (Server)',
114
+ value: nav.ttfb,
115
+ threshold: T.TTFB.POOR,
116
+ message: `High TTFB (${nav.ttfb}ms) due to slow server processing (${nav.serverProcessing}ms).`,
117
+ });
118
+ }
119
+ } else if (nav.ttfb > T.TTFB.MODERATE) {
120
+ scores.Backend += 3;
121
+ scores.Infra += 2;
122
+ evidence.push({
123
+ category: 'Backend',
124
+ severity: 'medium',
125
+ metric: 'TTFB',
126
+ value: nav.ttfb,
127
+ threshold: T.TTFB.MODERATE,
128
+ message: `Elevated TTFB (${nav.ttfb}ms). Room for improvement.`,
129
+ });
130
+ }
131
+
132
+ // =========================================
133
+ // BACKEND ANALYSIS
134
+ // =========================================
135
+
136
+ // API Latency - Average
137
+ if (api.avgLatency > T.API_LATENCY.POOR) {
138
+ scores.Backend += 10;
139
+ evidence.push({
140
+ category: 'Backend',
141
+ severity: 'high',
142
+ metric: 'API Latency (Avg)',
143
+ value: api.avgLatency,
144
+ threshold: T.API_LATENCY.POOR,
145
+ message: `Slow API responses (avg ${api.avgLatency}ms). Profile database queries and add caching.`,
146
+ });
147
+ } else if (api.avgLatency > T.API_LATENCY.MODERATE) {
148
+ scores.Backend += 4;
149
+ evidence.push({
150
+ category: 'Backend',
151
+ severity: 'medium',
152
+ metric: 'API Latency (Avg)',
153
+ value: api.avgLatency,
154
+ threshold: T.API_LATENCY.MODERATE,
155
+ message: `Moderate API latency (avg ${api.avgLatency}ms).`,
156
+ });
157
+ }
158
+
159
+ // API Latency - Max (single slow endpoint)
160
+ if (api.maxLatency > 500) {
161
+ scores.Backend += 5;
162
+ evidence.push({
163
+ category: 'Backend',
164
+ severity: 'medium',
165
+ metric: 'API Latency (Max)',
166
+ value: api.maxLatency,
167
+ threshold: 500,
168
+ message: `Slowest API call took ${api.maxLatency}ms. Identify and optimize this endpoint.`,
169
+ });
170
+ }
171
+
172
+ // Failed API Calls
173
+ if (api.failedCount > 0) {
174
+ scores.Backend += 6;
175
+ evidence.push({
176
+ category: 'Backend',
177
+ severity: 'high',
178
+ metric: 'Failed API Calls',
179
+ value: api.failedCount,
180
+ threshold: 0,
181
+ message: `${api.failedCount} API calls failed (4xx/5xx). Fix broken endpoints.`,
182
+ });
183
+ }
184
+
185
+ // =========================================
186
+ // FRONTEND ANALYSIS
187
+ // =========================================
188
+
189
+ // JavaScript Bundle Size
190
+ if (jsSizeMB > T.BUNDLE_SIZE_MB.POOR) {
191
+ scores.Frontend += 10;
192
+ evidence.push({
193
+ category: 'Frontend',
194
+ severity: 'high',
195
+ metric: 'JS Bundle Size',
196
+ value: jsSizeMB,
197
+ threshold: T.BUNDLE_SIZE_MB.POOR,
198
+ message: `Massive JS bundle (${jsSizeMB.toFixed(2)} MB). Implement code splitting and tree shaking.`,
199
+ });
200
+ } else if (jsSizeMB > T.BUNDLE_SIZE_MB.MODERATE) {
201
+ scores.Frontend += 5;
202
+ evidence.push({
203
+ category: 'Frontend',
204
+ severity: 'medium',
205
+ metric: 'JS Bundle Size',
206
+ value: jsSizeMB,
207
+ threshold: T.BUNDLE_SIZE_MB.MODERATE,
208
+ message: `Large JS bundle (${jsSizeMB.toFixed(2)} MB). Consider optimization.`,
209
+ });
210
+ }
211
+
212
+ // Total Blocking Time (Long Tasks)
213
+ if (vitals.totalBlockingTime > T.JS_EXECUTION.POOR) {
214
+ scores.Frontend += 10;
215
+ evidence.push({
216
+ category: 'Frontend',
217
+ severity: 'high',
218
+ metric: 'Total Blocking Time',
219
+ value: vitals.totalBlockingTime,
220
+ threshold: T.JS_EXECUTION.POOR,
221
+ message: `Heavy main thread blocking (${vitals.totalBlockingTime}ms). Move work to Web Workers.`,
222
+ });
223
+ } else if (vitals.totalBlockingTime > T.JS_EXECUTION.MODERATE) {
224
+ scores.Frontend += 4;
225
+ evidence.push({
226
+ category: 'Frontend',
227
+ severity: 'medium',
228
+ metric: 'Total Blocking Time',
229
+ value: vitals.totalBlockingTime,
230
+ threshold: T.JS_EXECUTION.MODERATE,
231
+ message: `Moderate main thread blocking (${vitals.totalBlockingTime}ms).`,
232
+ });
233
+ }
234
+
235
+ // Client Render Time (LCP - TTFB)
236
+ if (clientRenderTime > T.CLIENT_RENDER_TIME.POOR) {
237
+ scores.Frontend += 8;
238
+ evidence.push({
239
+ category: 'Frontend',
240
+ severity: 'high',
241
+ metric: 'Client Render Time',
242
+ value: clientRenderTime,
243
+ threshold: T.CLIENT_RENDER_TIME.POOR,
244
+ message: `Slow client rendering (${clientRenderTime}ms from TTFB to LCP). Optimize critical path.`,
245
+ });
246
+ }
247
+
248
+ // LCP (absolute)
249
+ if (vitals.largestContentfulPaint > T.LCP.POOR) {
250
+ scores.Frontend += 6;
251
+ evidence.push({
252
+ category: 'Frontend',
253
+ severity: 'high',
254
+ metric: 'LCP',
255
+ value: vitals.largestContentfulPaint,
256
+ threshold: T.LCP.POOR,
257
+ message: `Poor LCP (${vitals.largestContentfulPaint}ms). Largest element is loading too slowly.`,
258
+ });
259
+ } else if (vitals.largestContentfulPaint > T.LCP.GOOD) {
260
+ scores.Frontend += 3;
261
+ evidence.push({
262
+ category: 'Frontend',
263
+ severity: 'medium',
264
+ metric: 'LCP',
265
+ value: vitals.largestContentfulPaint,
266
+ threshold: T.LCP.GOOD,
267
+ message: `LCP needs improvement (${vitals.largestContentfulPaint}ms).`,
268
+ });
269
+ }
270
+
271
+ // Excessive API Calls (Frontend is requesting too much)
272
+ if (api.callCount > T.API_CALLS.EXCESSIVE) {
273
+ scores.Frontend += 8;
274
+ scores.Backend += 3;
275
+ evidence.push({
276
+ category: 'Frontend',
277
+ severity: 'high',
278
+ metric: 'API Call Count',
279
+ value: api.callCount,
280
+ threshold: T.API_CALLS.EXCESSIVE,
281
+ message: `Excessive API calls (${api.callCount}). Implement request batching or BFF pattern.`,
282
+ });
283
+ } else if (api.callCount > T.API_CALLS.HIGH) {
284
+ scores.Frontend += 4;
285
+ evidence.push({
286
+ category: 'Frontend',
287
+ severity: 'medium',
288
+ metric: 'API Call Count',
289
+ value: api.callCount,
290
+ threshold: T.API_CALLS.HIGH,
291
+ message: `High number of API calls (${api.callCount}). Consider aggregation.`,
292
+ });
293
+ }
294
+
295
+ // Render-blocking resources
296
+ if (metrics.renderBlocking.total > 5) {
297
+ scores.Frontend += 5;
298
+ evidence.push({
299
+ category: 'Frontend',
300
+ severity: 'medium',
301
+ metric: 'Render Blocking Resources',
302
+ value: metrics.renderBlocking.total,
303
+ threshold: 5,
304
+ message: `${metrics.renderBlocking.total} render-blocking resources. Use async/defer for scripts.`,
305
+ });
306
+ }
307
+
308
+ // Image Size
309
+ if (imageSizeMB > T.IMAGE_SIZE_MB.POOR) {
310
+ scores.Frontend += 6;
311
+ evidence.push({
312
+ category: 'Frontend',
313
+ severity: 'medium',
314
+ metric: 'Image Size',
315
+ value: imageSizeMB,
316
+ threshold: T.IMAGE_SIZE_MB.POOR,
317
+ message: `Heavy images (${imageSizeMB.toFixed(2)} MB). Use WebP/AVIF and lazy loading.`,
318
+ });
319
+ }
320
+
321
+ // =========================================
322
+ // DETERMINE VERDICT
323
+ // =========================================
324
+
325
+ const maxScore = Math.max(scores.Frontend, scores.Backend, scores.Infra);
326
+ let verdict;
327
+
328
+ if (maxScore < 3) {
329
+ verdict = 'Optimized';
330
+ } else {
331
+ verdict = Object.keys(scores).find(key => scores[key] === maxScore);
332
+
333
+ // Tie-breaking: If scores are close, report as "Mixed"
334
+ const sortedScores = Object.values(scores).sort((a, b) => b - a);
335
+ if (sortedScores[0] - sortedScores[1] < 3 && sortedScores[1] > 5) {
336
+ verdict = 'Mixed';
337
+ }
338
+ }
339
+
340
+ return {
341
+ verdict,
342
+ scores,
343
+ evidence,
344
+ metrics,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Generate recommendations based on verdict.
350
+ * @param {string} verdict - Primary bottleneck
351
+ * @param {array} evidence - Evidence items
352
+ * @returns {array} Recommendations
353
+ */
354
+ function getRecommendations(verdict, evidence) {
355
+ const recs = [];
356
+
357
+ // Specific recommendations based on evidence
358
+ const categories = evidence.map(e => e.category);
359
+
360
+ if (verdict === 'Frontend' || categories.includes('Frontend')) {
361
+ const hasLargeBundle = evidence.some(e => e.metric.includes('Bundle Size'));
362
+ const hasBlockingTime = evidence.some(e => e.metric.includes('Blocking'));
363
+ const hasRenderIssues = evidence.some(e => e.metric.includes('Render') || e.metric === 'LCP');
364
+ const hasChattyApi = evidence.some(e => e.metric.includes('API Call'));
365
+
366
+ if (hasLargeBundle) {
367
+ recs.push('Implement code splitting (dynamic imports) and tree shaking.');
368
+ recs.push('Analyze bundle with webpack-bundle-analyzer or source-map-explorer.');
369
+ }
370
+ if (hasBlockingTime) {
371
+ recs.push('Profile JavaScript with Chrome DevTools Performance tab.');
372
+ recs.push('Move heavy computations to Web Workers.');
373
+ }
374
+ if (hasRenderIssues) {
375
+ recs.push('Inline critical CSS and defer non-essential styles.');
376
+ recs.push('Preload LCP image/element with <link rel="preload">.');
377
+ }
378
+ if (hasChattyApi) {
379
+ recs.push('Implement a Backend-for-Frontend (BFF) to aggregate requests.');
380
+ recs.push('Consider GraphQL for flexible data fetching.');
381
+ }
382
+ }
383
+
384
+ if (verdict === 'Backend' || categories.includes('Backend')) {
385
+ const hasSlowApi = evidence.some(e => e.metric.includes('API Latency'));
386
+ const hasFailed = evidence.some(e => e.metric.includes('Failed'));
387
+
388
+ if (hasSlowApi) {
389
+ recs.push('Profile database queries - check for missing indexes and N+1 queries.');
390
+ recs.push('Add caching layer (Redis/Memcached) for frequent queries.');
391
+ recs.push('Consider denormalization or read replicas for heavy queries.');
392
+ }
393
+ if (hasFailed) {
394
+ recs.push('Fix failing endpoints immediately - check logs for errors.');
395
+ recs.push('Implement proper error handling and fallbacks.');
396
+ }
397
+ }
398
+
399
+ if (verdict === 'Infra' || categories.includes('Infra')) {
400
+ const hasDns = evidence.some(e => e.metric.includes('DNS'));
401
+ const hasTcp = evidence.some(e => e.metric.includes('TCP'));
402
+ const hasTtfb = evidence.some(e => e.metric.includes('TTFB'));
403
+
404
+ if (hasDns) {
405
+ recs.push('Use faster DNS provider (e.g., Cloudflare DNS).');
406
+ recs.push('Add <link rel="dns-prefetch"> for third-party domains.');
407
+ }
408
+ if (hasTcp) {
409
+ recs.push('Deploy servers/CDN edges closer to users.');
410
+ recs.push('Enable HTTP/2 or HTTP/3 for multiplexing.');
411
+ }
412
+ if (hasTtfb) {
413
+ recs.push('Review CDN configuration and cache hit rates.');
414
+ recs.push('Enable Brotli/Gzip compression on responses.');
415
+ recs.push('For serverless: investigate cold start optimization.');
416
+ }
417
+ }
418
+
419
+ if (verdict === 'Optimized') {
420
+ recs.push('Performance looks healthy! Consider implementing RUM for ongoing monitoring.');
421
+ recs.push('Set up alerting for performance regressions.');
422
+ }
423
+
424
+ if (verdict === 'Mixed') {
425
+ recs.push('Multiple areas need attention. Prioritize based on user impact.');
426
+ recs.push('Consider a holistic performance audit.');
427
+ }
428
+
429
+ return recs;
430
+ }
431
+
432
+ module.exports = { analyze, getRecommendations };