Marketplace: - Add skeleton loading states for stats grid and GPU offer cards - Show animated skeleton placeholders during data fetch - Add skeleton CSS with shimmer animation and dark mode support - Wrap stats section in #stats-grid container for skeleton injection Trade Exchange: - Replace inline header with data-global-header component - Switch GPU offers to production API (/api/miners/list) - Add fallback to demo
181 lines
6.0 KiB
JavaScript
181 lines
6.0 KiB
JavaScript
// Web Vitals monitoring for AITBC
|
|
// Tracks Core Web Vitals: LCP, FID, CLS, TTFB, FCP
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function sendToAnalytics(metric) {
|
|
const safeEntries = Array.isArray(metric.entries) ? metric.entries : [];
|
|
const safeValue = Number.isFinite(metric.value) ? Math.round(metric.value) : 0;
|
|
const safeDelta = Number.isFinite(metric.delta) ? Math.round(metric.delta) : 0;
|
|
|
|
const data = {
|
|
name: metric.name,
|
|
value: safeValue,
|
|
id: metric.id || 'unknown',
|
|
delta: safeDelta,
|
|
entries: safeEntries.map(e => ({
|
|
name: e.name,
|
|
startTime: e.startTime,
|
|
duration: e.duration
|
|
})),
|
|
url: window.location.href,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
const payload = JSON.stringify(data);
|
|
|
|
// Send to analytics endpoint
|
|
if (navigator.sendBeacon) {
|
|
const blob = new Blob([payload], { type: 'application/json' });
|
|
const ok = navigator.sendBeacon('/api/web-vitals', blob);
|
|
if (!ok) {
|
|
fetch('/api/web-vitals', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: payload,
|
|
keepalive: true
|
|
}).catch(() => {});
|
|
}
|
|
} else {
|
|
fetch('/api/web-vitals', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: payload,
|
|
keepalive: true
|
|
}).catch(() => {});
|
|
}
|
|
|
|
// Also log to console in development
|
|
console.log(`[Web Vitals] ${metric.name}: ${metric.value}`, metric);
|
|
}
|
|
|
|
// Largest Contentful Paint (LCP)
|
|
function observeLCP() {
|
|
try {
|
|
const observer = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
const lastEntry = entries[entries.length - 1];
|
|
sendToAnalytics({
|
|
name: 'LCP',
|
|
value: lastEntry.renderTime || lastEntry.loadTime,
|
|
id: lastEntry.id,
|
|
delta: 0,
|
|
entries: entries
|
|
});
|
|
});
|
|
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
|
} catch (e) {}
|
|
}
|
|
|
|
// First Input Delay (FID)
|
|
function observeFID() {
|
|
try {
|
|
const observer = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
entries.forEach(entry => {
|
|
sendToAnalytics({
|
|
name: 'FID',
|
|
value: entry.processingStart - entry.startTime,
|
|
id: entry.id,
|
|
delta: 0,
|
|
entries: [entry]
|
|
});
|
|
});
|
|
});
|
|
observer.observe({ entryTypes: ['first-input'] });
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Cumulative Layout Shift (CLS)
|
|
function observeCLS() {
|
|
try {
|
|
let clsValue = 0;
|
|
const observer = new PerformanceObserver((list) => {
|
|
try {
|
|
const entries = list.getEntries();
|
|
entries.forEach(entry => {
|
|
if (!entry.hadRecentInput) {
|
|
clsValue += entry.value;
|
|
}
|
|
});
|
|
sendToAnalytics({
|
|
name: 'CLS',
|
|
value: clsValue,
|
|
id: 'cls',
|
|
delta: 0,
|
|
entries: entries
|
|
});
|
|
} catch (e) {
|
|
// CLS measurement failed, but don't crash the whole script
|
|
console.log('CLS measurement error:', e.message);
|
|
}
|
|
});
|
|
|
|
// Try to observe layout-shift, but handle if it's not supported
|
|
try {
|
|
observer.observe({ entryTypes: ['layout-shift'] });
|
|
} catch (e) {
|
|
console.log('Layout-shift observation not supported:', e.message);
|
|
// CLS will not be measured, but other metrics will still work
|
|
}
|
|
} catch (e) {
|
|
console.log('CLS observer setup failed:', e.message);
|
|
}
|
|
}
|
|
|
|
// Time to First Byte (TTFB)
|
|
function measureTTFB() {
|
|
try {
|
|
const nav = performance.getEntriesByType('navigation')[0];
|
|
if (nav) {
|
|
sendToAnalytics({
|
|
name: 'TTFB',
|
|
value: nav.responseStart,
|
|
id: 'ttfb',
|
|
delta: 0,
|
|
entries: [nav]
|
|
});
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// First Contentful Paint (FCP)
|
|
function observeFCP() {
|
|
try {
|
|
const observer = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
entries.forEach(entry => {
|
|
if (entry.name === 'first-contentful-paint') {
|
|
sendToAnalytics({
|
|
name: 'FCP',
|
|
value: entry.startTime,
|
|
id: entry.id,
|
|
delta: 0,
|
|
entries: [entry]
|
|
});
|
|
}
|
|
});
|
|
});
|
|
observer.observe({ entryTypes: ['paint'] });
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Initialize when page loads
|
|
if (document.readyState === 'complete') {
|
|
observeLCP();
|
|
observeFID();
|
|
observeCLS();
|
|
observeFCP();
|
|
measureTTFB();
|
|
} else {
|
|
window.addEventListener('load', () => {
|
|
observeLCP();
|
|
observeFID();
|
|
observeCLS();
|
|
observeFCP();
|
|
measureTTFB();
|
|
});
|
|
}
|
|
})();
|