nextjs-performance-guard 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.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 nextjs-performance-guard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # nextjs-performance-guard
2
+
3
+ > A Next.js developer performance guard that detects heavy components, large bundles, and slow routes in development.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/nextjs-performance-guard.svg)](https://www.npmjs.com/package/nextjs-performance-guard)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## What Problem Does This Solve?
9
+
10
+ Performance issues in Next.js applications often go unnoticed until they reach production. By the time users experience slow load times, heavy components, or bloated bundles, it's too late. `nextjs-performance-guard` provides real-time performance warnings during development, helping you catch and fix issues before they ship.
11
+
12
+ ## Why Performance Guard Matters
13
+
14
+ - **Early Detection**: Catch performance regressions during development, not in production
15
+ - **Actionable Warnings**: Clear, debounced console warnings with specific metrics
16
+ - **Zero Production Impact**: Automatically disabled in production builds
17
+ - **Framework-Agnostic**: Works with both App Router and Pages Router
18
+ - **Lightweight**: No runtime dependencies, tree-shakable, ESM-only
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install --save-dev nextjs-performance-guard
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### App Router
29
+
30
+ #### 1. Heavy Component Detection
31
+
32
+ Wrap components that might be performance bottlenecks:
33
+
34
+ ```tsx
35
+ // app/components/DashboardCard.tsx
36
+ import { withPerformanceGuard } from 'nextjs-performance-guard';
37
+
38
+ function DashboardCard({ data }: { data: any }) {
39
+ // Your component logic
40
+ return <div>...</div>;
41
+ }
42
+
43
+ export default withPerformanceGuard(DashboardCard);
44
+ ```
45
+
46
+ #### 2. Client Bundle Size Guard
47
+
48
+ Add bundle analysis to your root layout:
49
+
50
+ ```tsx
51
+ // app/layout.tsx
52
+ import { analyzeClientBundle } from 'nextjs-performance-guard';
53
+ import { useEffect } from 'react';
54
+
55
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
56
+ useEffect(() => {
57
+ analyzeClientBundle({ maxSizeKB: 170 });
58
+ }, []);
59
+
60
+ return <html>{children}</html>;
61
+ }
62
+ ```
63
+
64
+ #### 3. Route Load Analyzer
65
+
66
+ Track route performance in your layout:
67
+
68
+ ```tsx
69
+ // app/layout.tsx
70
+ import { analyzeRoutes } from 'nextjs-performance-guard';
71
+ import { useEffect } from 'react';
72
+
73
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
74
+ useEffect(() => {
75
+ const analyzer = analyzeRoutes({ routeLoadThreshold: 1000 });
76
+
77
+ // Measure client-side navigation
78
+ return () => {
79
+ analyzer.measureNavigation();
80
+ };
81
+ }, []);
82
+
83
+ return <html>{children}</html>;
84
+ }
85
+ ```
86
+
87
+ ### Pages Router
88
+
89
+ #### 1. Heavy Component Detection
90
+
91
+ Same as App Router:
92
+
93
+ ```tsx
94
+ // components/HeavyComponent.tsx
95
+ import { withPerformanceGuard } from 'nextjs-performance-guard';
96
+
97
+ function HeavyComponent() {
98
+ return <div>...</div>;
99
+ }
100
+
101
+ export default withPerformanceGuard(HeavyComponent);
102
+ ```
103
+
104
+ #### 2. Client Bundle Size Guard
105
+
106
+ Add to `_app.tsx`:
107
+
108
+ ```tsx
109
+ // pages/_app.tsx
110
+ import { analyzeClientBundle } from 'nextjs-performance-guard';
111
+ import { useEffect } from 'react';
112
+
113
+ export default function App({ Component, pageProps }: AppProps) {
114
+ useEffect(() => {
115
+ analyzeClientBundle({ maxSizeKB: 170 });
116
+ }, []);
117
+
118
+ return <Component {...pageProps} />;
119
+ }
120
+ ```
121
+
122
+ #### 3. Route Load Analyzer
123
+
124
+ Add to `_app.tsx`:
125
+
126
+ ```tsx
127
+ // pages/_app.tsx
128
+ import { analyzeRoutes } from 'nextjs-performance-guard';
129
+ import { useEffect } from 'react';
130
+ import { useRouter } from 'next/router';
131
+
132
+ export default function App({ Component, pageProps }: AppProps) {
133
+ const router = useRouter();
134
+ const analyzer = analyzeRoutes({ routeLoadThreshold: 1000 });
135
+
136
+ useEffect(() => {
137
+ const handleRouteChange = () => {
138
+ analyzer.measureNavigation();
139
+ };
140
+
141
+ router.events.on('routeChangeComplete', handleRouteChange);
142
+ return () => {
143
+ router.events.off('routeChangeComplete', handleRouteChange);
144
+ };
145
+ }, [router]);
146
+
147
+ return <Component {...pageProps} />;
148
+ }
149
+ ```
150
+
151
+ ## Configuration Options
152
+
153
+ All functions accept an optional `PerformanceGuardConfig`:
154
+
155
+ ```typescript
156
+ interface PerformanceGuardConfig {
157
+ enabled?: boolean; // Override NODE_ENV check (default: auto)
158
+ componentThreshold?: number; // Render time threshold in ms (default: 16)
159
+ bundleThresholdKB?: number; // Bundle size threshold in KB (default: 170)
160
+ routeLoadThreshold?: number; // Route load threshold in ms (default: 1000)
161
+ debounceMs?: number; // Warning debounce time in ms (default: 1000-2000)
162
+ }
163
+ ```
164
+
165
+ ### Example with Custom Configuration
166
+
167
+ ```tsx
168
+ import { withPerformanceGuard } from 'nextjs-performance-guard';
169
+
170
+ export default withPerformanceGuard(MyComponent, {
171
+ componentThreshold: 32, // Warn if render > 32ms
172
+ debounceMs: 2000, // Debounce warnings by 2s
173
+ });
174
+ ```
175
+
176
+ ## Warning Examples
177
+
178
+ ### Heavy Component
179
+
180
+ ```
181
+ ⚠️ [Next Perf Guard] Heavy component detected: DashboardCard (render: 68ms) (re-renders: 15)
182
+ ```
183
+
184
+ ### Large Bundle
185
+
186
+ ```
187
+ ⚠️ [Next Perf Guard] Client bundle exceeds 170kb on /dashboard (actual: 245kb)
188
+ ```
189
+
190
+ ### Slow Route
191
+
192
+ ```
193
+ ⚠️ [Next Perf Guard] Slow route detected: /profile (load: 1.9s) (hydration: 0.8s)
194
+ ```
195
+
196
+ ## Dev-Only Behavior
197
+
198
+ `nextjs-performance-guard` automatically disables itself when:
199
+
200
+ - `NODE_ENV === "production"`
201
+ - Running in a production build
202
+ - Server-side rendering (SSR) contexts
203
+
204
+ All performance monitoring logic is guarded behind development checks, ensuring **zero runtime overhead** in production.
205
+
206
+ ## API Reference
207
+
208
+ ### `withPerformanceGuard<T>(Component, config?)`
209
+
210
+ Wraps a React component to monitor render performance.
211
+
212
+ **Returns**: The same component type, wrapped with performance monitoring.
213
+
214
+ ### `analyzeClientBundle(config?)`
215
+
216
+ Analyzes the current page's client-side JavaScript bundle size.
217
+
218
+ **Returns**: `Promise<BundleMetrics | null>` (null in production or if under threshold).
219
+
220
+ ### `analyzeRoutes(config?)`
221
+
222
+ Creates a route analyzer instance that tracks load times, hydration delays, and navigation latency.
223
+
224
+ **Returns**: `RouteAnalyzer` instance with methods:
225
+ - `getMetrics(route?)`: Get metrics for a specific route or all routes
226
+ - `measureNavigation()`: Manually trigger navigation measurement
227
+ - `reset()`: Clear all collected metrics
228
+
229
+ ### Reset Functions
230
+
231
+ - `resetComponentGuard()`: Clear component metrics
232
+ - `resetBundleGuard()`: Clear bundle analysis cache
233
+ - `resetRouteAnalyzer()`: Clear route metrics
234
+
235
+ ## Limitations
236
+
237
+ - **Development Only**: Performance monitoring only works in development mode
238
+ - **Client-Side Only**: Bundle and route analysis require browser APIs
239
+ - **Heuristic-Based**: Component file size estimation is approximate
240
+ - **Next.js 13+**: Requires Next.js 13 or higher (App Router or Pages Router)
241
+ - **React 18+**: Requires React 18 or higher
242
+
243
+ ## License
244
+
245
+ MIT
246
+
247
+ ## Contributing
248
+
249
+ Contributions welcome! Please open an issue or pull request.
250
+
251
+
@@ -0,0 +1,6 @@
1
+ import type { BundleMetrics, PerformanceGuardConfig } from './types.js';
2
+ export declare function analyzeClientBundle(config?: PerformanceGuardConfig & {
3
+ maxSizeKB?: number;
4
+ }): Promise<BundleMetrics | null>;
5
+ export declare function resetBundleGuard(): void;
6
+ //# sourceMappingURL=bundleGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundleGuard.d.ts","sourceRoot":"","sources":["../src/bundleGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AA+IxE,wBAAsB,mBAAmB,CACvC,MAAM,CAAC,EAAE,sBAAsB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAU/B;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC"}
@@ -0,0 +1,134 @@
1
+ const isDevelopment = () => {
2
+ return typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
3
+ };
4
+ const defaultConfig = {
5
+ bundleThresholdKB: 170,
6
+ debounceMs: 2000,
7
+ };
8
+ class BundleGuard {
9
+ analyzedRoutes = new Set();
10
+ lastWarning = new Map();
11
+ config;
12
+ constructor(config = {}) {
13
+ this.config = {
14
+ bundleThresholdKB: config.bundleThresholdKB ?? defaultConfig.bundleThresholdKB,
15
+ debounceMs: config.debounceMs ?? defaultConfig.debounceMs,
16
+ };
17
+ }
18
+ shouldWarn(route) {
19
+ if (!isDevelopment())
20
+ return false;
21
+ const now = Date.now();
22
+ const lastWarnTime = this.lastWarning.get(route) ?? 0;
23
+ return now - lastWarnTime > this.config.debounceMs;
24
+ }
25
+ async getScriptSizes() {
26
+ if (typeof window === 'undefined') {
27
+ return { totalKB: 0, scripts: [] };
28
+ }
29
+ const scripts = Array.from(document.querySelectorAll('script[src]'));
30
+ const scriptData = [];
31
+ let totalBytes = 0;
32
+ for (const script of scripts) {
33
+ const src = script.src;
34
+ if (!src || !src.includes('_next/static')) {
35
+ continue;
36
+ }
37
+ try {
38
+ const response = await fetch(src, { method: 'HEAD' });
39
+ const contentLength = response.headers.get('content-length');
40
+ if (contentLength) {
41
+ const sizeBytes = parseInt(contentLength, 10);
42
+ const sizeKB = sizeBytes / 1024;
43
+ totalBytes += sizeBytes;
44
+ scriptData.push({ src, sizeKB: Math.round(sizeKB * 100) / 100 });
45
+ }
46
+ }
47
+ catch {
48
+ try {
49
+ const response = await fetch(src);
50
+ const blob = await response.blob();
51
+ const sizeKB = blob.size / 1024;
52
+ totalBytes += blob.size;
53
+ scriptData.push({ src, sizeKB: Math.round(sizeKB * 100) / 100 });
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ }
59
+ }
60
+ return {
61
+ totalKB: Math.round((totalBytes / 1024) * 100) / 100,
62
+ scripts: scriptData,
63
+ };
64
+ }
65
+ getCurrentRoute() {
66
+ if (typeof window === 'undefined') {
67
+ return '/';
68
+ }
69
+ try {
70
+ return window.location.pathname;
71
+ }
72
+ catch {
73
+ return '/';
74
+ }
75
+ }
76
+ async analyzeClientBundle(config) {
77
+ if (!isDevelopment()) {
78
+ return null;
79
+ }
80
+ const thresholdKB = config?.maxSizeKB ?? config?.bundleThresholdKB ?? this.config.bundleThresholdKB;
81
+ const route = this.getCurrentRoute();
82
+ if (this.analyzedRoutes.has(route)) {
83
+ return null;
84
+ }
85
+ this.analyzedRoutes.add(route);
86
+ try {
87
+ const { totalKB } = await this.getScriptSizes();
88
+ if (totalKB > thresholdKB) {
89
+ if (this.shouldWarn(route)) {
90
+ const metrics = {
91
+ route,
92
+ sizeKB: totalKB,
93
+ thresholdKB,
94
+ };
95
+ this.warn(metrics);
96
+ this.lastWarning.set(route, Date.now());
97
+ return metrics;
98
+ }
99
+ }
100
+ }
101
+ catch (error) {
102
+ if (isDevelopment()) {
103
+ console.debug('[Next Perf Guard] Bundle analysis failed:', error);
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ warn(metrics) {
109
+ const parts = [];
110
+ parts.push(`⚠️ [Next Perf Guard] Client bundle exceeds ${metrics.thresholdKB}kb`);
111
+ parts.push(`on ${metrics.route}`);
112
+ parts.push(`(actual: ${metrics.sizeKB}kb)`);
113
+ console.warn(parts.join(' '));
114
+ }
115
+ reset() {
116
+ this.analyzedRoutes.clear();
117
+ this.lastWarning.clear();
118
+ }
119
+ }
120
+ let globalBundleGuard = null;
121
+ export async function analyzeClientBundle(config) {
122
+ if (!isDevelopment()) {
123
+ return null;
124
+ }
125
+ if (!globalBundleGuard) {
126
+ globalBundleGuard = new BundleGuard(config);
127
+ }
128
+ return globalBundleGuard.analyzeClientBundle(config);
129
+ }
130
+ export function resetBundleGuard() {
131
+ if (globalBundleGuard) {
132
+ globalBundleGuard.reset();
133
+ }
134
+ }
@@ -0,0 +1,4 @@
1
+ import type { PerformanceGuardConfig } from './types.js';
2
+ export declare function withPerformanceGuard<T extends (...args: any[]) => any>(Component: T, config?: PerformanceGuardConfig): T;
3
+ export declare function resetComponentGuard(): void;
4
+ //# sourceMappingURL=componentGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"componentGuard.d.ts","sourceRoot":"","sources":["../src/componentGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAuJ3E,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACpE,SAAS,EAAE,CAAC,EACZ,MAAM,CAAC,EAAE,sBAAsB,GAC9B,CAAC,CAUH;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C"}
@@ -0,0 +1,134 @@
1
+ const isDevelopment = () => {
2
+ return typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
3
+ };
4
+ const defaultConfig = {
5
+ componentThreshold: 16, // ms (60fps = 16.67ms per frame)
6
+ debounceMs: 1000,
7
+ };
8
+ class ComponentGuard {
9
+ renderTimes = new Map();
10
+ renderCounts = new Map();
11
+ lastWarning = new Map();
12
+ config;
13
+ constructor(config = {}) {
14
+ this.config = {
15
+ componentThreshold: config.componentThreshold ?? defaultConfig.componentThreshold,
16
+ debounceMs: config.debounceMs ?? defaultConfig.debounceMs,
17
+ };
18
+ }
19
+ shouldWarn(componentName) {
20
+ if (!isDevelopment())
21
+ return false;
22
+ const now = Date.now();
23
+ const lastWarnTime = this.lastWarning.get(componentName) ?? 0;
24
+ return now - lastWarnTime > this.config.debounceMs;
25
+ }
26
+ recordRender(componentName, renderTime) {
27
+ if (!isDevelopment())
28
+ return;
29
+ const times = this.renderTimes.get(componentName) ?? [];
30
+ times.push(renderTime);
31
+ if (times.length > 10) {
32
+ times.shift();
33
+ }
34
+ this.renderTimes.set(componentName, times);
35
+ const count = this.renderCounts.get(componentName) ?? 0;
36
+ this.renderCounts.set(componentName, count + 1);
37
+ }
38
+ getAverageRenderTime(componentName) {
39
+ const times = this.renderTimes.get(componentName) ?? [];
40
+ if (times.length === 0)
41
+ return 0;
42
+ return times.reduce((a, b) => a + b, 0) / times.length;
43
+ }
44
+ estimateFileSize(componentName) {
45
+ if (typeof window === 'undefined')
46
+ return undefined;
47
+ try {
48
+ const component = window.__NEXT_DATA__?.props?.pageProps;
49
+ if (!component)
50
+ return undefined;
51
+ const scripts = document.querySelectorAll('script[src]');
52
+ let totalSize = 0;
53
+ scripts.forEach((script) => {
54
+ const src = script.getAttribute('src');
55
+ if (src && src.includes('_next/static')) {
56
+ const sizeMatch = src.match(/\/_next\/static\/chunks\/(\d+)/);
57
+ if (sizeMatch) {
58
+ totalSize += parseInt(sizeMatch[1], 10);
59
+ }
60
+ }
61
+ });
62
+ return totalSize > 0 ? totalSize : undefined;
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }
68
+ measureRender(Component, componentName) {
69
+ if (!isDevelopment()) {
70
+ return Component;
71
+ }
72
+ const name = componentName || Component.displayName || Component.name || 'Unknown';
73
+ const GuardedComponent = ((...args) => {
74
+ const renderStart = performance.now();
75
+ const result = Component(...args);
76
+ const renderEnd = performance.now();
77
+ const renderTime = renderEnd - renderStart;
78
+ this.recordRender(name, renderTime);
79
+ const avgRenderTime = this.getAverageRenderTime(name);
80
+ const renderCount = this.renderCounts.get(name) ?? 0;
81
+ if (renderTime > this.config.componentThreshold ||
82
+ (avgRenderTime > this.config.componentThreshold && renderCount > 5)) {
83
+ if (this.shouldWarn(name)) {
84
+ const fileSize = this.estimateFileSize(name);
85
+ const metrics = {
86
+ name,
87
+ renderTime: Math.round(renderTime * 100) / 100,
88
+ renderCount,
89
+ fileSize,
90
+ };
91
+ this.warn(metrics);
92
+ this.lastWarning.set(name, Date.now());
93
+ }
94
+ }
95
+ return result;
96
+ });
97
+ GuardedComponent.displayName = `withPerformanceGuard(${name})`;
98
+ return GuardedComponent;
99
+ }
100
+ warn(metrics) {
101
+ const parts = [];
102
+ parts.push(`⚠️ [Next Perf Guard] Heavy component detected: ${metrics.name}`);
103
+ if (metrics.renderTime > this.config.componentThreshold) {
104
+ parts.push(`(render: ${metrics.renderTime}ms)`);
105
+ }
106
+ if (metrics.renderCount > 10) {
107
+ parts.push(`(re-renders: ${metrics.renderCount})`);
108
+ }
109
+ if (metrics.fileSize && metrics.fileSize > 100000) {
110
+ parts.push(`(estimated size: ${Math.round(metrics.fileSize / 1024)}kb)`);
111
+ }
112
+ console.warn(parts.join(' '));
113
+ }
114
+ reset() {
115
+ this.renderTimes.clear();
116
+ this.renderCounts.clear();
117
+ this.lastWarning.clear();
118
+ }
119
+ }
120
+ let globalGuard = null;
121
+ export function withPerformanceGuard(Component, config) {
122
+ if (!isDevelopment()) {
123
+ return Component;
124
+ }
125
+ if (!globalGuard) {
126
+ globalGuard = new ComponentGuard(config);
127
+ }
128
+ return globalGuard.measureRender(Component);
129
+ }
130
+ export function resetComponentGuard() {
131
+ if (globalGuard) {
132
+ globalGuard.reset();
133
+ }
134
+ }
@@ -0,0 +1,5 @@
1
+ export { withPerformanceGuard, resetComponentGuard } from './componentGuard.js';
2
+ export { analyzeClientBundle, resetBundleGuard } from './bundleGuard.js';
3
+ export { analyzeRoutes, resetRouteAnalyzer } from './routeAnalyzer.js';
4
+ export type { PerformanceGuardConfig, ComponentMetrics, BundleMetrics, RouteMetrics, WarningOptions, WarningType, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACvE,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,WAAW,GACZ,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { withPerformanceGuard, resetComponentGuard } from './componentGuard.js';
2
+ export { analyzeClientBundle, resetBundleGuard } from './bundleGuard.js';
3
+ export { analyzeRoutes, resetRouteAnalyzer } from './routeAnalyzer.js';
@@ -0,0 +1,21 @@
1
+ import type { RouteMetrics, PerformanceGuardConfig } from './types.js';
2
+ declare class RouteAnalyzer {
3
+ private routeMetrics;
4
+ private lastWarning;
5
+ private navigationStartTime;
6
+ private hydrationStartTime;
7
+ private config;
8
+ constructor(config?: PerformanceGuardConfig);
9
+ private setupListeners;
10
+ private shouldWarn;
11
+ private getCurrentRoute;
12
+ private measureRouteLoad;
13
+ measureNavigation(): void;
14
+ private warn;
15
+ getMetrics(route?: string): RouteMetrics | Map<string, RouteMetrics>;
16
+ reset(): void;
17
+ }
18
+ export declare function analyzeRoutes(config?: PerformanceGuardConfig): RouteAnalyzer;
19
+ export declare function resetRouteAnalyzer(): void;
20
+ export {};
21
+ //# sourceMappingURL=routeAnalyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeAnalyzer.d.ts","sourceRoot":"","sources":["../src/routeAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAWvE,cAAM,aAAa;IACjB,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,MAAM,CAA8E;gBAEhF,MAAM,GAAE,sBAA2B;IAW/C,OAAO,CAAC,cAAc;IA8BtB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,gBAAgB;IAqCxB,iBAAiB,IAAI,IAAI;IA+BzB,OAAO,CAAC,IAAI;IAmBZ,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAOpE,KAAK,IAAI,IAAI;CAMd;AAID,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,sBAAsB,GAAG,aAAa,CAc5E;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAIzC"}
@@ -0,0 +1,167 @@
1
+ const isDevelopment = () => {
2
+ return typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
3
+ };
4
+ const defaultConfig = {
5
+ routeLoadThreshold: 1000,
6
+ debounceMs: 2000,
7
+ };
8
+ class RouteAnalyzer {
9
+ routeMetrics = new Map();
10
+ lastWarning = new Map();
11
+ navigationStartTime = null;
12
+ hydrationStartTime = null;
13
+ config;
14
+ constructor(config = {}) {
15
+ this.config = {
16
+ routeLoadThreshold: config.routeLoadThreshold ?? defaultConfig.routeLoadThreshold,
17
+ debounceMs: config.debounceMs ?? defaultConfig.debounceMs,
18
+ };
19
+ if (isDevelopment() && typeof window !== 'undefined') {
20
+ this.setupListeners();
21
+ }
22
+ }
23
+ setupListeners() {
24
+ if (typeof window === 'undefined')
25
+ return;
26
+ window.addEventListener('load', () => {
27
+ this.measureRouteLoad();
28
+ });
29
+ if (typeof document !== 'undefined') {
30
+ document.addEventListener('DOMContentLoaded', () => {
31
+ this.hydrationStartTime = performance.now();
32
+ });
33
+ }
34
+ if (typeof window !== 'undefined' && window.next?.router) {
35
+ const router = window.next.router;
36
+ const originalPush = router.push;
37
+ const originalReplace = router.replace;
38
+ router.push = (...args) => {
39
+ this.navigationStartTime = performance.now();
40
+ return originalPush.apply(router, args);
41
+ };
42
+ router.replace = (...args) => {
43
+ this.navigationStartTime = performance.now();
44
+ return originalReplace.apply(router, args);
45
+ };
46
+ }
47
+ }
48
+ shouldWarn(route) {
49
+ if (!isDevelopment())
50
+ return false;
51
+ const now = Date.now();
52
+ const lastWarnTime = this.lastWarning.get(route) ?? 0;
53
+ return now - lastWarnTime > this.config.debounceMs;
54
+ }
55
+ getCurrentRoute() {
56
+ if (typeof window === 'undefined') {
57
+ return '/';
58
+ }
59
+ try {
60
+ return window.location.pathname;
61
+ }
62
+ catch {
63
+ return '/';
64
+ }
65
+ }
66
+ measureRouteLoad() {
67
+ if (!isDevelopment() || typeof window === 'undefined') {
68
+ return;
69
+ }
70
+ const route = this.getCurrentRoute();
71
+ try {
72
+ const navigation = performance.getEntriesByType('navigation')[0];
73
+ const loadTime = navigation ? navigation.loadEventEnd - navigation.fetchStart : 0;
74
+ let hydrationTime;
75
+ if (this.hydrationStartTime) {
76
+ hydrationTime = performance.now() - this.hydrationStartTime;
77
+ }
78
+ const metrics = {
79
+ route,
80
+ loadTime: Math.round(loadTime),
81
+ hydrationTime: hydrationTime ? Math.round(hydrationTime) : undefined,
82
+ };
83
+ this.routeMetrics.set(route, metrics);
84
+ if (loadTime > this.config.routeLoadThreshold) {
85
+ if (this.shouldWarn(route)) {
86
+ this.warn(metrics);
87
+ this.lastWarning.set(route, Date.now());
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ if (isDevelopment()) {
93
+ console.debug('[Next Perf Guard] Route load measurement failed:', error);
94
+ }
95
+ }
96
+ }
97
+ measureNavigation() {
98
+ if (!isDevelopment() || typeof window === 'undefined') {
99
+ return;
100
+ }
101
+ if (this.navigationStartTime === null) {
102
+ return;
103
+ }
104
+ const route = this.getCurrentRoute();
105
+ const navigationTime = performance.now() - this.navigationStartTime;
106
+ const existing = this.routeMetrics.get(route);
107
+ const metrics = {
108
+ route,
109
+ loadTime: existing?.loadTime ?? 0,
110
+ hydrationTime: existing?.hydrationTime,
111
+ navigationTime: Math.round(navigationTime),
112
+ };
113
+ this.routeMetrics.set(route, metrics);
114
+ this.navigationStartTime = null;
115
+ if (navigationTime > this.config.routeLoadThreshold) {
116
+ if (this.shouldWarn(route)) {
117
+ this.warn(metrics);
118
+ this.lastWarning.set(route, Date.now());
119
+ }
120
+ }
121
+ }
122
+ warn(metrics) {
123
+ const parts = [];
124
+ parts.push(`⚠️ [Next Perf Guard] Slow route detected: ${metrics.route}`);
125
+ if (metrics.loadTime > this.config.routeLoadThreshold) {
126
+ parts.push(`(load: ${(metrics.loadTime / 1000).toFixed(1)}s)`);
127
+ }
128
+ if (metrics.hydrationTime && metrics.hydrationTime > 500) {
129
+ parts.push(`(hydration: ${(metrics.hydrationTime / 1000).toFixed(1)}s)`);
130
+ }
131
+ if (metrics.navigationTime && metrics.navigationTime > this.config.routeLoadThreshold) {
132
+ parts.push(`(navigation: ${(metrics.navigationTime / 1000).toFixed(1)}s)`);
133
+ }
134
+ console.warn(parts.join(' '));
135
+ }
136
+ getMetrics(route) {
137
+ if (route) {
138
+ return this.routeMetrics.get(route) ?? { route, loadTime: 0 };
139
+ }
140
+ return new Map(this.routeMetrics);
141
+ }
142
+ reset() {
143
+ this.routeMetrics.clear();
144
+ this.lastWarning.clear();
145
+ this.navigationStartTime = null;
146
+ this.hydrationStartTime = null;
147
+ }
148
+ }
149
+ let globalRouteAnalyzer = null;
150
+ export function analyzeRoutes(config) {
151
+ if (!isDevelopment()) {
152
+ return {
153
+ getMetrics: () => new Map(),
154
+ reset: () => { },
155
+ measureNavigation: () => { },
156
+ };
157
+ }
158
+ if (!globalRouteAnalyzer) {
159
+ globalRouteAnalyzer = new RouteAnalyzer(config);
160
+ }
161
+ return globalRouteAnalyzer;
162
+ }
163
+ export function resetRouteAnalyzer() {
164
+ if (globalRouteAnalyzer) {
165
+ globalRouteAnalyzer.reset();
166
+ }
167
+ }
@@ -0,0 +1,31 @@
1
+ export interface PerformanceGuardConfig {
2
+ enabled?: boolean;
3
+ componentThreshold?: number;
4
+ bundleThresholdKB?: number;
5
+ routeLoadThreshold?: number;
6
+ debounceMs?: number;
7
+ }
8
+ export interface ComponentMetrics {
9
+ name: string;
10
+ renderTime: number;
11
+ renderCount: number;
12
+ fileSize?: number;
13
+ }
14
+ export interface BundleMetrics {
15
+ route: string;
16
+ sizeKB: number;
17
+ thresholdKB: number;
18
+ }
19
+ export interface RouteMetrics {
20
+ route: string;
21
+ loadTime: number;
22
+ hydrationTime?: number;
23
+ navigationTime?: number;
24
+ }
25
+ export interface WarningOptions {
26
+ component?: ComponentMetrics;
27
+ bundle?: BundleMetrics;
28
+ route?: RouteMetrics;
29
+ }
30
+ export type WarningType = 'component' | 'bundle' | 'route';
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "nextjs-performance-guard",
3
+ "version": "1.0.0",
4
+ "description": "A Next.js developer performance guard that detects heavy components, large bundles, and slow routes in development",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "nextjs",
25
+ "next.js",
26
+ "performance",
27
+ "monitoring",
28
+ "development",
29
+ "bundle-size",
30
+ "react",
31
+ "performance-guard"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/your-username/nextjs-performance-guard.git"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "peerDependencies": {
43
+ "next": ">=13.0.0",
44
+ "react": ">=18.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.0.0",
48
+ "@types/react": "^18.0.0",
49
+ "typescript": "^5.0.0"
50
+ }
51
+ }
52
+
53
+