jgeiser's picture
Wicklee fleet demo (static build from main)
01560d7
Raw History Blame Contribute Delete
9.48 kB
/* Load-diagnostics overlay. Loaded by /fonts.js only when the URL carries
* `perf` (e.g. https://wicklee.dev/pricing?perf=1); ordinary visitors never
* fetch this file.
*
* Phones have no Web Inspector, but every browser keeps a performance timeline
* of its own: navigation timing, one resource-timing entry per request, paint
* and largest-contentful-paint entries, long tasks (Chromium), and the `wk:*`
* User Timing marks the app records as it boots. This script reads that
* timeline a moment after `load` and renders it as a plain-text report with
* Copy and Share buttons, so the numbers from the actual device on the actual
* network can be pasted into a chat or an issue.
*
* Cross-origin resources (Clerk, Paddle, Google Fonts) expose start and end
* times but hide sizes and wait/download split unless they send
* Timing-Allow-Origin β€” those show as `?`. The waterfall shape is still there.
*/
(function () {
'use strict';
if (!window.performance || !performance.getEntriesByType) return;
try { performance.setResourceTimingBufferSize(600); } catch (e) { /* optional */ }
var lcp = null, longTasks = [], shift = 0;
function observe(type, cb) {
try {
var po = new PerformanceObserver(function (list) { list.getEntries().forEach(cb); });
po.observe({ type: type, buffered: true });
return true;
} catch (e) { return false; }
}
var hasLcp = observe('largest-contentful-paint', function (e) { lcp = e; });
var hasLongTask = observe('longtask', function (e) { longTasks.push(e); });
observe('layout-shift', function (e) { if (!e.hadRecentInput) shift += e.value; });
function ms(v) { return (v == null || isNaN(v)) ? '-' : Math.round(v) + 'ms'; }
function kb(b) { return b ? (b / 1024).toFixed(b < 10240 ? 1 : 0) + 'kB' : '?'; }
function pad(s, n) { s = String(s); while (s.length < n) s += ' '; return s; }
function lpad(s, n) { s = String(s); while (s.length < n) s = ' ' + s; return s; }
function short(url) {
try {
var u = new URL(url), p = u.pathname;
if (p.length > 52) p = p.slice(0, 24) + '…' + p.slice(-26);
return (u.host === location.host ? '' : u.host) + p;
} catch (e) { return url; }
}
function report() {
var L = [];
var nav = performance.getEntriesByType('navigation')[0];
var c = navigator.connection || {};
L.push('Wicklee load report ' + new Date().toISOString());
L.push('url: ' + location.href.replace(/[?&#]perf(=[^&#]*)?/, ''));
L.push('ua: ' + navigator.userAgent);
L.push('viewport: ' + innerWidth + 'x' + innerHeight + ' @' + devicePixelRatio + 'x' +
(c.effectiveType ? ' net: ' + c.effectiveType + (c.downlink ? ' ' + c.downlink + 'Mbps' : '') + (c.rtt ? ' rtt ' + c.rtt + 'ms' : '') : ' net: (not exposed by this browser)'));
if (nav) {
L.push('');
L.push('navigation (' + nav.type + (nav.deliveryType ? ', ' + nav.deliveryType : '') + '):');
L.push(' redirect ' + ms(nav.redirectEnd - nav.redirectStart) +
' dns ' + ms(nav.domainLookupEnd - nav.domainLookupStart) +
' connect ' + ms(nav.connectEnd - nav.connectStart) +
' ttfb ' + ms(nav.responseStart) +
' html ' + ms(nav.responseEnd - nav.responseStart) +
' ' + kb(nav.transferSize) + (!nav.transferSize && nav.decodedBodySize ? ' (cache)' : ''));
L.push(' domInteractive ' + ms(nav.domInteractive) +
' DOMContentLoaded ' + ms(nav.domContentLoadedEventEnd) +
' load ' + (nav.loadEventEnd ? ms(nav.loadEventEnd) : '(not yet)'));
}
L.push('');
L.push('paint:');
performance.getEntriesByType('paint').forEach(function (p) { L.push(' ' + pad(p.name, 26) + ms(p.startTime)); });
if (lcp) {
var el = lcp.element;
L.push(' ' + pad('largest-contentful-paint', 26) + ms(lcp.startTime) +
(el ? ' <' + el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') + '> ' + (el.textContent || el.currentSrc || '').trim().slice(0, 40) : ''));
} else {
L.push(' largest-contentful-paint ' + (hasLcp ? '(none recorded yet)' : '(not supported by this browser)'));
}
var marks = performance.getEntriesByType('mark').filter(function (m) { return m.name.indexOf('wk:') === 0; });
if (marks.length) {
L.push('');
L.push('app marks:');
marks.forEach(function (m) { L.push(' ' + pad(m.name.slice(3), 20) + ms(m.startTime)); });
}
if (document.getElementById('wk-prerender')) {
L.push(' !! prerendered block still in the DOM β€” React has not mounted');
}
L.push('');
if (hasLongTask) {
var tot = 0, max = 0;
longTasks.forEach(function (t) { tot += t.duration; if (t.duration > max) max = t.duration; });
L.push('main thread: ' + longTasks.length + ' long tasks, total ' + ms(tot) + ', longest ' + ms(max) + ' layout shift ' + shift.toFixed(3));
} else {
L.push('main thread: long tasks not exposed by this browser layout shift ' + shift.toFixed(3));
}
L.push('');
var res = performance.getEntriesByType('resource').slice().sort(function (a, b) { return a.startTime - b.startTime; });
L.push('resources (' + res.length + '): start→end wait/download size type url');
res.forEach(function (r) {
var timing = r.responseStart
? ms(r.responseStart - r.startTime) + '/' + ms(r.responseEnd - r.responseStart)
: ms(r.duration) + ' (opaque)';
var size = r.transferSize ? kb(r.transferSize) : (r.decodedBodySize ? 'cache' : '?');
L.push(' ' + lpad(Math.round(r.startTime), 5) + 'β†’' + pad(Math.round(r.responseEnd), 5) +
' ' + pad(timing, 15) + ' ' + pad(size, 7) + ' ' + pad(r.initiatorType || '', 8) + ' ' + short(r.name) +
(r.renderBlockingStatus === 'blocking' ? ' [render-blocking]' : ''));
});
return L.join('\n');
}
// ── UI ────────────────────────────────────────────────────────────────────
var root = document.createElement('div');
root.id = 'wk-perf';
var S = root.style;
S.position = 'fixed'; S.left = '0'; S.right = '0'; S.bottom = '0'; S.zIndex = '2147483647';
S.maxHeight = '62vh'; S.display = 'flex'; S.flexDirection = 'column';
S.background = '#0b1220'; S.color = '#e5e7eb'; S.borderTop = '2px solid #3b82f6';
S.font = '12px/1.4 -apple-system, system-ui, sans-serif'; S.boxShadow = '0 -8px 30px rgba(0,0,0,.5)';
var bar = document.createElement('div');
bar.style.cssText = 'display:flex;gap:8px;align-items:center;padding:8px 10px;flex:none;border-bottom:1px solid #1f2937';
var title = document.createElement('span');
title.textContent = 'Load report';
title.style.cssText = 'font-weight:600;margin-right:auto';
bar.appendChild(title);
function button(label, onClick) {
var b = document.createElement('button');
b.type = 'button'; b.textContent = label;
b.style.cssText = 'appearance:none;border:1px solid #374151;background:#1f2937;color:#f9fafb;border-radius:6px;padding:6px 10px;font:inherit;font-weight:500';
b.addEventListener('click', onClick);
bar.appendChild(b);
return b;
}
var pre = document.createElement('pre');
pre.style.cssText = 'margin:0;padding:10px;overflow:auto;flex:1;font:10px/1.45 ui-monospace,Menlo,monospace;white-space:pre;-webkit-user-select:text;user-select:text;-webkit-overflow-scrolling:touch';
var pill = document.createElement('button');
pill.type = 'button'; pill.textContent = 'perf';
pill.style.cssText = 'position:fixed;right:12px;bottom:12px;z-index:2147483647;appearance:none;border:0;border-radius:999px;padding:8px 14px;background:#3b82f6;color:#fff;font:600 12px system-ui,sans-serif;box-shadow:0 4px 14px rgba(0,0,0,.4);display:none';
pill.addEventListener('click', show);
function refresh() { pre.textContent = report(); }
function show() { refresh(); pill.style.display = 'none'; if (!root.isConnected) document.body.appendChild(root); }
function hide() { if (root.isConnected) root.remove(); pill.style.display = ''; }
var copyBtn = button('Copy', function () {
var text = pre.textContent;
function done(ok) { copyBtn.textContent = ok ? 'Copied βœ“' : 'Select text to copy'; setTimeout(function () { copyBtn.textContent = 'Copy'; }, 2500); }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { fallback(); });
} else fallback();
function fallback() {
try {
var range = document.createRange(); range.selectNodeContents(pre);
var sel = getSelection(); sel.removeAllRanges(); sel.addRange(range);
done(document.execCommand('copy'));
} catch (e) { done(false); }
}
});
if (navigator.share) {
button('Share', function () {
navigator.share({ title: 'Wicklee load report', text: pre.textContent }).catch(function () { /* user cancelled */ });
});
}
button('Refresh', refresh);
button('Hide', hide);
root.appendChild(bar);
root.appendChild(pre);
document.documentElement.appendChild(pill);
// Open a beat after `load` so late arrivals (ClerkJS, fonts, the lazy app
// chunk) are in the list. Refresh re-reads the timeline at any time.
function armed() { setTimeout(show, 2500); }
if (document.readyState === 'complete') armed(); else addEventListener('load', armed);
})();