@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 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,5 @@
1
+ export * from './components';
2
+ export * from './providers';
3
+ export * from './utils';
4
+
5
+ export type { MetricRating } from './utils';
@@ -0,0 +1,6 @@
1
+ export {
2
+ VitalsMonitoringProvider,
3
+ useVitalsContext,
4
+ VitalsMonitoringContext,
5
+ } from './vitalsMonitor';
6
+ export type { IMonitoringContextType, VitalsMonitoringProviderProps } from './vitalsMonitor';
@@ -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
+ }