PageSourceSearch

https://uni.xyz/static/assets/js/uni-vitals.js?v=2

js uni.xyz collected 2026-10-01 11:40:29 UTC 5,304 bytes, 129 lines download raw bytes

1/*!
2 * uni-vitals: real-user Core Web Vitals, with the element attribution that
3 * Search Console cannot give us.
4 *
5 * Added 2026-09-30. Search Console reported CLS 0.90 across 1,365
6 * /journals/:slug URLs, and that number could not be reproduced on the SSR
7 * path or the SPA path, at full speed or throttled to 400kbps with a 4x CPU
8 * slowdown. CrUX tells you a page shifts; it never tells you what moved.
9 * This does.
10 *
11 * No dependency on purpose: the web-vitals package would have to be bundled
12 * into twenty Angular apps, and this file is served straight from /static so
13 * it can change without rebuilding any of them.
14 *
15 * Deliberately cheap: three PerformanceObservers, one sendBeacon when the
16 * page is hidden, nothing on the critical path, and it fails silently.
17 */
18(function () {
19  'use strict';
20  try {
21    if (!window.PerformanceObserver || !navigator.sendBeacon) return;
22    // Never measure prerendered or restored-from-bfcache loads: their timings
23    // are not what a visitor experienced.
24    if (document.prerendering) return;
25
26    var ENDPOINT = '/api/v2/vitals/';
27    var sent = false;
28    var cls = { value: 0, element: '' };
29    var lcp = { value: 0, element: '' };
30    var inp = { value: 0, element: '' };
31
32    function describe(node) {
33      if (!node || node.nodeType !== 1) return '';
34      var tag = (node.tagName || '').toLowerCase();
35      var id = node.id ? '#' + node.id : '';
36      var cn = '';
37      if (node.className && typeof node.className === 'string') {
38        cn = '.' + node.className.trim().split(/\s+/).slice(0, 3).join('.');
39      }
40      return (tag + id + cn).slice(0, 120);
41    }
42
43    function observe(type, cb, opts) {
44      try {
45        var o = new PerformanceObserver(function (l) { l.getEntries().forEach(cb); });
46        o.observe(Object.assign({ type: type, buffered: true }, opts || {}));
47        return o;
48      } catch (e) { return null; }
49    }
50
51    // CLS is the largest SESSION WINDOW, not the running total: a window ends
52    // after a 1s gap or 5s of wall clock. Summing every shift over the life of
53    // a long-lived SPA page overstates it, and the number then cannot be
54    // compared against Search Console, which is the entire point of this file.
55    var winValue = 0, winFirst = 0, winLast = 0, winWorst = 0, winElement = '';
56    observe('layout-shift', function (e) {
57      if (e.hadRecentInput) return;
58      if (winValue && e.startTime - winLast < 1000 && e.startTime - winFirst < 5000) {
59        winValue += e.value;
60        winLast = e.startTime;
61      } else {
62        winValue = e.value;
63        winFirst = winLast = e.startTime;
64        winWorst = 0;
65        winElement = '';
66      }
67      if (e.value > winWorst) {
68        winWorst = e.value;
69        var src = (e.sources || [])[0];
70        if (src && src.node) winElement = describe(src.node);
71      }
72      if (winValue > cls.value) {
73        cls.value = winValue;
74        cls.element = winElement;
75      }
76    });
77
78    // LCP stops at the first interaction or when the page is first hidden.
79    // Without this a tab left open in the background reports its LCP as
80    // however long it sat there: the first samples this file collected
81    // included a 53,304ms "LCP", which is a backgrounded tab, not a page load.
82    var lcpFrozen = false;
83    observe('largest-contentful-paint', function (e) {
84      if (lcpFrozen) return;
85      lcp.value = e.startTime;
86      lcp.element = describe(e.element) || (e.url ? ('url:' + e.url.slice(-60)) : '');
87    });
88    function freezeLcp() { lcpFrozen = true; }
89    ['keydown', 'click', 'pointerdown'].forEach(function (t) {
90      addEventListener(t, freezeLcp, { once: true, capture: true });
91    });
92
93    // INP approximated by the worst qualifying interaction, which is what the
94    // p75 of this metric is driven by in practice.
95    observe('event', function (e) {
96      if (!e.interactionId || e.duration <= inp.value) return;
97      inp.value = e.duration;
98      inp.element = describe(e.target);
99    }, { durationThreshold: 40 });
100
101    function send() {
102      if (sent) return;
103      sent = true;
104      try {
105        var v = [];
106        if (cls.value > 0) v.push({ n: 'CLS', v: +cls.value.toFixed(4), e: cls.element });
107        if (lcp.value > 0) v.push({ n: 'LCP', v: Math.round(lcp.value), e: lcp.element });
108        if (inp.value > 0) v.push({ n: 'INP', v: Math.round(inp.value), e: inp.element });
109        if (!v.length) return;
110        var conn = navigator.connection && navigator.connection.effectiveType;
111        var body = JSON.stringify({
112          p: location.pathname,
113          m: window.matchMedia && window.matchMedia('(max-width: 768px)').matches,
114          c: conn || '',
115          v: v
116        });
117        navigator.sendBeacon(ENDPOINT, new Blob([body], { type: 'application/json' }));
118      } catch (e) { /* a beacon must never break the page */ }
119    }
120
121    // visibilitychange, not unload: unload does not fire reliably on mobile
122    // and is what makes naive RUM under-report phones, the devices that
123    // actually fail these metrics.
124    addEventListener('visibilitychange', function () {
125      if (document.visibilityState === 'hidden') { freezeLcp(); send(); }
126    }, true);
127    addEventListener('pagehide', send, true);
128  } catch (e) { /* never break a page over measurement */ }
129})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.