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 +23 -0
- package/README.md +251 -0
- package/dist/bundleGuard.d.ts +6 -0
- package/dist/bundleGuard.d.ts.map +1 -0
- package/dist/bundleGuard.js +134 -0
- package/dist/componentGuard.d.ts +4 -0
- package/dist/componentGuard.d.ts.map +1 -0
- package/dist/componentGuard.js +134 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/routeAnalyzer.d.ts +21 -0
- package/dist/routeAnalyzer.d.ts.map +1 -0
- package/dist/routeAnalyzer.js +167 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
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
|
+
[](https://www.npmjs.com/package/nextjs-performance-guard)
|
|
6
|
+
[](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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
|