@capsuletech/web-profiler 0.1.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/README.md +70 -0
- package/package.json +18 -0
- package/src/components/dashboard.tsx +75 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +5 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/vitalsMonitor.tsx +123 -0
- package/src/utils.ts +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @capsuletech/profiler
|
|
2
|
+
|
|
3
|
+
Performance monitoring and profiling utilities for SolidJS applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 Web Vitals tracking (CLS, FCP, INP, LCP, TTFB)
|
|
8
|
+
- 📊 Real-time performance metrics dashboard
|
|
9
|
+
- 💾 Memory usage monitoring
|
|
10
|
+
- 📡 Network performance tracking
|
|
11
|
+
- ⚡ Lightweight and non-intrusive
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @capsuletech/profiler
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### VitalsMonitoringProvider
|
|
22
|
+
|
|
23
|
+
Wrap your application with the provider to start collecting metrics:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { VitalsMonitoringProvider } from '@capsuletech/profiler/providers';
|
|
27
|
+
|
|
28
|
+
export default function App() {
|
|
29
|
+
return (
|
|
30
|
+
<VitalsMonitoringProvider>
|
|
31
|
+
<YourComponent />
|
|
32
|
+
</VitalsMonitoringProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Using Metrics in Components
|
|
38
|
+
|
|
39
|
+
Access metrics from the context:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useVitalsContext } from '@capsuletech/profiler/providers';
|
|
43
|
+
|
|
44
|
+
function MyComponent() {
|
|
45
|
+
const context = useVitalsContext();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<p>Performance Metrics Available</p>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Monitored Metrics
|
|
56
|
+
|
|
57
|
+
- **FCP** - First Contentful Paint
|
|
58
|
+
- **LCP** - Largest Contentful Paint
|
|
59
|
+
- **CLS** - Cumulative Layout Shift
|
|
60
|
+
- **INP** - Interaction to Next Paint
|
|
61
|
+
- **TTFB** - Time to First Byte
|
|
62
|
+
- **Memory Usage** - JavaScript heap usage
|
|
63
|
+
- **Network Load** - Total network data transferred
|
|
64
|
+
- **Bundle Size** - Total resource bundle size
|
|
65
|
+
- **Connection Type** - Network connection speed
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
70
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capsuletech/web-profiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"!**/*.tsbuildinfo"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": {},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"solid-js": "^1.9.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@capsuletech/shared-vite": "0.1.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { For, type JSX, Show } from 'solid-js';
|
|
2
|
+
import { getRating } from '../utils';
|
|
3
|
+
|
|
4
|
+
interface DashboardProps {
|
|
5
|
+
metrics: Record<string, number>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Dashboard(props: DashboardProps) {
|
|
9
|
+
const entries = () => Object.entries(props.metrics);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Show when={entries().length > 0}>
|
|
13
|
+
<div
|
|
14
|
+
style={{
|
|
15
|
+
position: 'fixed',
|
|
16
|
+
top: '15px',
|
|
17
|
+
right: '15px',
|
|
18
|
+
'background-color': 'rgba(15, 15, 15, 0.95)',
|
|
19
|
+
color: '#fff',
|
|
20
|
+
padding: '12px',
|
|
21
|
+
'border-radius': '10px',
|
|
22
|
+
'font-size': '11px',
|
|
23
|
+
'z-index': '10000',
|
|
24
|
+
'font-family': 'monospace',
|
|
25
|
+
'min-width': '260px',
|
|
26
|
+
border: '1px solid #333',
|
|
27
|
+
'box-shadow': '0 10px 30px rgba(0,0,0,0.5)',
|
|
28
|
+
'pointer-events': 'none',
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
'font-weight': 'bold',
|
|
34
|
+
'margin-bottom': '8px',
|
|
35
|
+
'border-bottom': '1px solid #333',
|
|
36
|
+
'padding-bottom': '5px',
|
|
37
|
+
color: '#00d4ff',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
🚀 PERFORMANCE MONITOR
|
|
41
|
+
</div>
|
|
42
|
+
<For each={entries()}>
|
|
43
|
+
{(entry) => {
|
|
44
|
+
const [key, val] = entry;
|
|
45
|
+
const isNumeric = typeof val === 'number';
|
|
46
|
+
const rating = isNumeric
|
|
47
|
+
? getRating(key, val)
|
|
48
|
+
: { label: 'INFO' as const, color: '#3498db', unit: '' };
|
|
49
|
+
const isFloat = key.includes('CLS') || key.includes('Load') || key.includes('Bundle');
|
|
50
|
+
const formattedValue = isNumeric ? val.toFixed(isFloat ? 2 : 0) : val;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
display: 'flex',
|
|
56
|
+
'justify-content': 'space-between',
|
|
57
|
+
'margin-bottom': '6px',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<span style={{ color: '#aaa' }}>{key}:</span>
|
|
61
|
+
<div style={{ 'text-align': 'right' }}>
|
|
62
|
+
<span style={{ color: rating.color, 'font-weight': 'bold' }}>
|
|
63
|
+
{formattedValue}
|
|
64
|
+
<span style={{ 'font-size': '9px', 'margin-left': '4px' }}>{rating.unit}</span>
|
|
65
|
+
</span>
|
|
66
|
+
<div style={{ 'font-size': '8px', opacity: 0.6 }}>{rating.label}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}}
|
|
71
|
+
</For>
|
|
72
|
+
</div>
|
|
73
|
+
</Show>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dashboard } from './dashboard';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JSX,
|
|
3
|
+
createContext,
|
|
4
|
+
createEffect,
|
|
5
|
+
createMemo,
|
|
6
|
+
createSignal,
|
|
7
|
+
onCleanup,
|
|
8
|
+
useContext,
|
|
9
|
+
} from 'solid-js';
|
|
10
|
+
import { Dashboard } from '../components';
|
|
11
|
+
import {
|
|
12
|
+
getConnectionType,
|
|
13
|
+
getDomReadyTime,
|
|
14
|
+
getMemoryMetrics,
|
|
15
|
+
getNetworkMetrics,
|
|
16
|
+
setupWebVitalsTracking,
|
|
17
|
+
} from '../utils';
|
|
18
|
+
|
|
19
|
+
export interface IMonitoringContextType {
|
|
20
|
+
updateComponentMetric: (name: string, value: number | string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const VitalsMonitoringContext = createContext<IMonitoringContextType | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
export interface VitalsMonitoringProviderProps {
|
|
26
|
+
children: JSX.Element;
|
|
27
|
+
showDashboard?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function VitalsMonitoringProvider(props: VitalsMonitoringProviderProps) {
|
|
31
|
+
const [displayMetrics, setDisplayMetrics] = createSignal<Record<string, number>>({});
|
|
32
|
+
const metricsRef: Record<string, number> = {};
|
|
33
|
+
let rafId: number | null = null;
|
|
34
|
+
const showDashboard = () => props.showDashboard !== false; // default true
|
|
35
|
+
|
|
36
|
+
const updateComponentMetric = (name: string, value: number | string) => {
|
|
37
|
+
if (metricsRef[name] === value) return;
|
|
38
|
+
|
|
39
|
+
if (typeof value === 'number') {
|
|
40
|
+
metricsRef[name] = value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (rafId === null) {
|
|
44
|
+
rafId = requestAnimationFrame(() => {
|
|
45
|
+
setDisplayMetrics({ ...metricsRef });
|
|
46
|
+
rafId = null;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
createEffect(() => {
|
|
52
|
+
// Setup Web Vitals tracking
|
|
53
|
+
const handleVitals = (metric: any) => {
|
|
54
|
+
updateComponentMetric(metric.name, metric.value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
setupWebVitalsTracking(handleVitals);
|
|
58
|
+
|
|
59
|
+
// Initial resource metrics
|
|
60
|
+
const updateResourceMetrics = () => {
|
|
61
|
+
const { network, bundle } = getNetworkMetrics();
|
|
62
|
+
updateComponentMetric('📡 Network Load', network);
|
|
63
|
+
updateComponentMetric('📦 Total Bundle', bundle);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
updateResourceMetrics();
|
|
67
|
+
setTimeout(updateResourceMetrics, 2000);
|
|
68
|
+
|
|
69
|
+
// Memory monitoring
|
|
70
|
+
const memoryInterval = setInterval(() => {
|
|
71
|
+
const mem = getMemoryMetrics();
|
|
72
|
+
if (mem !== null) {
|
|
73
|
+
updateComponentMetric('💻 Memory Usage', mem);
|
|
74
|
+
}
|
|
75
|
+
}, 2000);
|
|
76
|
+
|
|
77
|
+
// DOM ready time
|
|
78
|
+
const domTime = getDomReadyTime();
|
|
79
|
+
if (domTime !== null) {
|
|
80
|
+
updateComponentMetric('⏱️ Dom Ready', domTime);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Connection type
|
|
84
|
+
const connection = getConnectionType();
|
|
85
|
+
if (connection !== 'unknown') {
|
|
86
|
+
updateComponentMetric('🌐 Network', connection);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Performance Observer for new resources
|
|
90
|
+
const observer = new PerformanceObserver(() => {
|
|
91
|
+
updateResourceMetrics();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
observer.observe({ entryTypes: ['resource'] });
|
|
96
|
+
} catch {
|
|
97
|
+
// Some browsers may not support resource timing
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onCleanup(() => {
|
|
101
|
+
clearInterval(memoryInterval);
|
|
102
|
+
observer.disconnect();
|
|
103
|
+
if (rafId !== null) {
|
|
104
|
+
cancelAnimationFrame(rafId);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const contextValue = createMemo(() => ({
|
|
110
|
+
updateComponentMetric,
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<VitalsMonitoringContext.Provider value={contextValue()}>
|
|
115
|
+
{props.children}
|
|
116
|
+
{showDashboard() && <Dashboard metrics={displayMetrics()} />}
|
|
117
|
+
</VitalsMonitoringContext.Provider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useVitalsContext(): IMonitoringContextType | undefined {
|
|
122
|
+
return useContext(VitalsMonitoringContext);
|
|
123
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';
|
|
2
|
+
|
|
3
|
+
export interface MetricRating {
|
|
4
|
+
label: 'GOOD' | 'NEEDS_IMPROVEMENT' | 'POOR' | 'INFO';
|
|
5
|
+
color: string;
|
|
6
|
+
unit: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getRating(metricName: string, value: number): MetricRating {
|
|
10
|
+
// Web Vitals thresholds
|
|
11
|
+
// https://web.dev/vitals/
|
|
12
|
+
|
|
13
|
+
if (metricName.includes('FCP')) {
|
|
14
|
+
// First Contentful Paint: Good < 1.8s
|
|
15
|
+
if (value < 1800) return { label: 'GOOD', color: '#10b981', unit: 'ms' };
|
|
16
|
+
if (value < 3000) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: 'ms' };
|
|
17
|
+
return { label: 'POOR', color: '#ef4444', unit: 'ms' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (metricName.includes('LCP')) {
|
|
21
|
+
// Largest Contentful Paint: Good < 2.5s
|
|
22
|
+
if (value < 2500) return { label: 'GOOD', color: '#10b981', unit: 'ms' };
|
|
23
|
+
if (value < 4000) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: 'ms' };
|
|
24
|
+
return { label: 'POOR', color: '#ef4444', unit: 'ms' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (metricName.includes('CLS')) {
|
|
28
|
+
// Cumulative Layout Shift: Good < 0.1
|
|
29
|
+
if (value < 0.1) return { label: 'GOOD', color: '#10b981', unit: '' };
|
|
30
|
+
if (value < 0.25) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: '' };
|
|
31
|
+
return { label: 'POOR', color: '#ef4444', unit: '' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (metricName.includes('INP')) {
|
|
35
|
+
// Interaction to Next Paint: Good < 200ms
|
|
36
|
+
if (value < 200) return { label: 'GOOD', color: '#10b981', unit: 'ms' };
|
|
37
|
+
if (value < 500) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: 'ms' };
|
|
38
|
+
return { label: 'POOR', color: '#ef4444', unit: 'ms' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (metricName.includes('TTFB')) {
|
|
42
|
+
// Time to First Byte: Good < 800ms
|
|
43
|
+
if (value < 800) return { label: 'GOOD', color: '#10b981', unit: 'ms' };
|
|
44
|
+
if (value < 1800) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: 'ms' };
|
|
45
|
+
return { label: 'POOR', color: '#ef4444', unit: 'ms' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Memory and Network
|
|
49
|
+
if (metricName.includes('Memory')) {
|
|
50
|
+
// Less than 50MB is good
|
|
51
|
+
if (value < 50) return { label: 'GOOD', color: '#10b981', unit: 'MB' };
|
|
52
|
+
if (value < 100) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: 'MB' };
|
|
53
|
+
return { label: 'POOR', color: '#ef4444', unit: 'MB' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (metricName.includes('Network') || metricName.includes('Bundle')) {
|
|
57
|
+
if (value < 1) return { label: 'GOOD', color: '#10b981', unit: 'MB' };
|
|
58
|
+
if (value < 3) return { label: 'NEEDS_IMPROVEMENT', color: '#f59e0b', unit: 'MB' };
|
|
59
|
+
return { label: 'POOR', color: '#ef4444', unit: 'MB' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { label: 'INFO', color: '#3498db', unit: '' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setupWebVitalsTracking(onMetric: (metric: Metric) => void) {
|
|
66
|
+
onCLS(onMetric, { reportAllChanges: true });
|
|
67
|
+
onLCP(onMetric, { reportAllChanges: true });
|
|
68
|
+
onFCP(onMetric, { reportAllChanges: true });
|
|
69
|
+
onTTFB(onMetric);
|
|
70
|
+
onINP(onMetric, { reportAllChanges: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getNetworkMetrics() {
|
|
74
|
+
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
|
75
|
+
|
|
76
|
+
let totalNetwork = 0;
|
|
77
|
+
let totalBundle = 0;
|
|
78
|
+
|
|
79
|
+
for (const res of resources) {
|
|
80
|
+
totalNetwork += res.transferSize;
|
|
81
|
+
const actualSize = res.decodedBodySize || res.encodedBodySize || res.transferSize;
|
|
82
|
+
totalBundle += actualSize;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
network: totalNetwork / 1024 / 1024, // MB
|
|
87
|
+
bundle: totalBundle / 1024 / 1024, // MB
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getMemoryMetrics(): number | null {
|
|
92
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
93
|
+
const mem = (performance as any).memory;
|
|
94
|
+
if (!mem) return null;
|
|
95
|
+
return Math.round(mem.usedJSHeapSize / 1024 / 1024);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getConnectionType(): string {
|
|
99
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
100
|
+
const connection = (navigator as any).connection;
|
|
101
|
+
if (!connection) return 'unknown';
|
|
102
|
+
return connection.effectiveType || 'unknown';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getDomReadyTime(): number | null {
|
|
106
|
+
const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
107
|
+
if (!navEntry) return null;
|
|
108
|
+
return navEntry.domContentLoadedEventEnd;
|
|
109
|
+
}
|