UNPKG

boomerangjs

Version:

boomerang always comes back, except when it hits something

559 lines (462 loc) 15.1 kB
/** * The PaintTiming plugin collects paint metrics exposed by the W3C * [Paint Timing]{@link https://www.w3.org/TR/paint-timing/} and * [Largest Contentful Paint]{@link https://wicg.github.io/largest-contentful-paint/} * specifications. * * For information on how to include this plugin, see the {@tutorial building} tutorial. * * ## Prerendering * * The following beacon parameters are affected by Prerendering and will be offset by the * `activationStart` time (if any): * * * `pt.fp` (First Paint) * * `pt.fcp` (First Contentful Paint) * * `pt.lcp` (Largest Contentful Paint) * * ## Beacon Parameters * * All beacon parameters are prefixed with `pt.`. * * This plugin adds the following parameters to the beacon: * * * `pt.fp`: `first-paint` in `DOMHighResTimestamp` * * `pt.fcp`: `first-contentful-paint` in `DOMHighResTimestamp` * * `pt.lcp`: `largest-contentful-paint` in `DOMHighResTimestamp` * * `pt.hid`: The document was loaded hidden (at some point), so FP and FCP are * user-driven events, and thus won't be added to the beacon. * * `pt.lcp.src`: Source URL of the Largest Contentful Paint element * * `pt.lcp.el`: Element tag name of the Largest Contentful Paint * * `pt.lcp.id`: Element ID of the Largest Contentful Paint * * `pt.lcp.e`: Element Pseudo-CSS selector for the Largest Contentful Paint * * `pt.lcp.srcset`: Element srcset property of the Largest Contentful Paint * * `pt.lcp.sizes`: Element sizes property of the Largest Contentful Paint * * `pt.lcp.s`: Size of the Largest Contentful Paint in device-independent pixels squared * * @see {@link https://www.w3.org/TR/paint-timing/} * @see {@link https://wicg.github.io/largest-contentful-paint/} * @class BOOMR.plugins.PaintTiming */ (function() { BOOMR = window.BOOMR || {}; BOOMR.plugins = BOOMR.plugins || {}; if (BOOMR.plugins.PaintTiming) { return; } /** * Map of Paint Timing API names to `pt.*` beacon parameters * * https://www.w3.org/TR/paint-timing/ */ var PAINT_TIMING_MAP = { "first-paint": "fp", "first-contentful-paint": "fcp", "largest-contentful-paint": "lcp" }; /** * Private implementation */ var impl = { /** * Whether or not we've initialized yet */ initialized: false, /** * Whether or not we've added data to the beacon */ complete: false, /** * Whether or not the browser supports PaintTiming (cached value) */ supported: null, /** * Whether or not the browser supports Soft Navigation Heuristics */ supportedSoftNavHeuristics: null, /** * Cached PaintTiming values */ timingCache: {}, /* BEGIN_DEBUG */ /** * History of timings */ timingHistory: {}, /* END_DEBUG */ /** * LCP observer */ observer: null, // Metrics that will be exported externalMetrics: {}, // the largest contentful paint at the moment lcp: { // render time time: 0, // tag name el: "", // src / href src: "", // element ID id: "", // pseudo-css selector e: "", // srcset attribute srcset: "", // sizes attribute sizes: "", // size s: 0 }, // keeps track if onBeforeBeacon has sent lcp data once already lcpDataSent: false, /** * Executed on `page_ready`, `xhr_load` and `before_unload` */ done: function(edata, ename) { var p, paintTimings, i; if (this.complete) { // we've already added data to the beacon return this; } // // Don't add PaintTimings to SPA Soft or XHR beacons -- // Only add to Page Load (ename: load) and SPA Hard (ename: xhr // and initiator: spa_hard) beacons. // if (ename !== "load" && (!edata || edata.initiator !== "spa_hard")) { this.complete = true; return this; } p = BOOMR.getPerformance(); if (!p || typeof p.getEntriesByType !== "function") { // can't do anything if window.performance isn't available this.complete = true; return; } // // Get First Paint, First Contentful Paint, etc from Paint Timing API // https://www.w3.org/TR/paint-timing/ // paintTimings = p.getEntriesByType("paint"); if (paintTimings && paintTimings.length) { BOOMR.info("This user agent supports PaintTiming", "pt"); for (i = 0; i < paintTimings.length; i++) { // cache it for others who want to use it impl.timingCache[paintTimings[i].name] = paintTimings[i].startTime; if (PAINT_TIMING_MAP[paintTimings[i].name]) { // get the timestamp, offset by Prerendered, if it happened var ts = BOOMR.getPrerenderedOffset(paintTimings[i].startTime); // add pt.* to a single beacon BOOMR.addVar( "pt." + PAINT_TIMING_MAP[paintTimings[i].name], ts, true); } } this.complete = true; BOOMR.sendBeacon(); } }, /** * `before_beacon` listener for adding Largest Contentful Paint * to beacon data. Allows LCP to be added to both Page Load and * Early beacons, while avoiding sending it on Error beacons, * SPA Soft Navigations, or sending redundant LCPs. */ onBeforeBeacon: function(data){ if (!BOOMR.isPageLoadBeacon(data)) { // we don't want to send LCP data on beacons not related to page load return; } if (impl.lcpDataSent) { // prevents LCP data from being sent redundantly return; } // // LCP isn't supported for Soft Navigations (unless Soft Nav Heuristics are enabled), // or any other non-Page Load beacons, so don't listen any more if LCP hasn't happened by // the Page Load beacon. // if (!data.early && impl.observer && !impl.supportedSoftNavHeuristics) { impl.observer.disconnect(); impl.observer = null; } if (!impl.lcp.time) { // time is used to check if LCP has been populated return; } // get the timestamp, offset by Prerendered, if it happened BOOMR.addVar("pt.lcp", BOOMR.getPrerenderedOffset(impl.lcp.time), true); if (impl.lcp.src) { BOOMR.addVar("pt.lcp.src", impl.lcp.src, true); } if (impl.lcp.el) { BOOMR.addVar("pt.lcp.el", impl.lcp.el, true); } if (impl.lcp.id) { BOOMR.addVar("pt.lcp.id", impl.lcp.id, true); } if (impl.lcp.e) { BOOMR.addVar("pt.lcp.e", impl.lcp.e, true); } if (impl.lcp.srcset) { BOOMR.addVar("pt.lcp.srcset", impl.lcp.srcset, true); } if (impl.lcp.sizes) { BOOMR.addVar("pt.lcp.sizes", impl.lcp.sizes, true); } if (impl.lcp.s) { BOOMR.addVar("pt.lcp.s", impl.lcp.s, true); } if (!data.early) { impl.lcpDataSent = true; } }, /** * Performance observer callback for LCP * * @param {PerformanceEntry[]} list Performance entries */ onObserver: function(list) { var entries = list.getEntries(); if (entries.length === 0) { return; } // Use the latest one var lcp = entries[entries.length - 1]; // LCP can change over time, so always take the latest value. Use renderTime // if available (for same-origin resources or if they have Timing-Allow-Origin), // otherwise loadTime is the best we can get. impl.lcp.time = lcp.renderTime || lcp.loadTime; // cache it for others who want to use it impl.timingCache[lcp.entryType] = impl.lcp.time; // size impl.lcp.s = lcp.size ? lcp.size : 0; // prioritize getting the source URL directly from the event if (lcp.url) { impl.lcp.src = lcp.url; } if (lcp.element) { // tag name impl.lcp.el = lcp.element.tagName; // src / href if (!impl.lcp.src) { impl.lcp.src = (lcp.element.href || lcp.element.src) || ""; } // element ID impl.lcp.id = lcp.element.id || ""; // Pseudo-CSS selector impl.lcp.e = BOOMR.utils.makeSelector(lcp.element); // srcset attribute impl.lcp.srcset = lcp.element.srcset || ""; // sizes attribute impl.lcp.sizes = lcp.element.sizes || ""; } // don't bring data: URI src URLs if (impl.lcp.src && impl.lcp.src.indexOf("data:") === 0) { // gather the image type if we can var semiIdx = impl.lcp.src.indexOf(";"); // replace without the actual data: impl.lcp.src = semiIdx !== -1 ? impl.lcp.src.substr(0, semiIdx) : "data:"; } /* BEGIN_DEBUG */ /** * History of timings */ impl.timingHistory[lcp.entryType] = impl.timingHistory[lcp.entryType] || []; impl.timingHistory[lcp.entryType].push({ time: impl.lcp.time, src: impl.lcp.src, el: impl.lcp.el, id: impl.lcp.id, e: impl.lcp.e, srcset: impl.lcp.srcset, sizes: impl.lcp.sizes, s: impl.lcp.s }); /* END_DEBUG */ }, /** * Soft Navigation Observer * * If a Soft Nav happens, we can expect LCP updates. * * @param {PerformanceEntry[]} list Entries */ onSoftNavObserver: function(list) { var entries = list.getEntries(); if (entries.length === 0) { return; } BOOMR.debug("Detected a Soft Navigation, resetting LCP data", "PaintTiming"); impl.lcpDataSent = false; } }; // // Exports // BOOMR.plugins.PaintTiming = { /** * Initializes the plugin. * * This plugin does not have any configuration. * * @returns {@link BOOMR.plugins.PaintTiming} The PaintTiming plugin for chaining * @memberof BOOMR.plugins.PaintTiming */ init: function() { // skip initialization if not supported if (!this.is_supported()) { impl.complete = true; impl.initialized = true; } // If we haven't added PaintTiming data and the page is currently // hidden, don't add anything to the beacon as the paint might // happen only when the visitor makes the document visible. if (!impl.complete && BOOMR.visibilityState() === "hidden") { BOOMR.addVar("pt.hid", 1, true); impl.complete = true; } if (!impl.initialized) { // we'll add data to the beacon on whichever happens first BOOMR.subscribe("page_ready", impl.done, "load", impl); BOOMR.subscribe("xhr_load", impl.done, "xhr", impl); BOOMR.subscribe("before_unload", impl.done, null, impl); BOOMR.subscribe("before_beacon", impl.onBeforeBeacon, null, impl); // create a PO for LCP if (typeof BOOMR.window.PerformanceObserver === "function" && typeof BOOMR.window.LargestContentfulPaint === "function") { impl.observer = new BOOMR.window.PerformanceObserver(impl.onObserver); // check for support of Soft Navigation Heuristics if (typeof BOOMR.window.SoftNavigationEntry === "function") { impl.supportedSoftNavHeuristics = true; // add an observer impl.softNavObserver = new BOOMR.window.PerformanceObserver(impl.onSoftNavObserver); impl.softNavObserver.observe({ type: "soft-navigation" }); } var options = { type: "largest-contentful-paint", buffered: true }; // listen to soft navs to know if we should 'reset' our expected state if (impl.supportedSoftNavHeuristics) { options.includeSoftNavigationObservations = true; } impl.observer.observe(options); } impl.initialized = true; } return this; }, /** * Whether or not this plugin is complete * * @returns {boolean} `true` if the plugin is complete * @memberof BOOMR.plugins.PaintTiming */ is_complete: function() { return true; }, /** * Whether or not this plugin is enabled and PaintTiming is supported. * * @returns {boolean} `true` if PaintTiming plugin is enabled and supported. * @memberof BOOMR.plugins.PaintTiming */ is_enabled: function() { return impl.initialized && this.is_supported(); }, /** * Whether or not PaintTiming is supported in this browser. * * @returns {boolean} `true` if PaintTiming is supported. * @memberof BOOMR.plugins.PaintTiming */ is_supported: function() { var p; if (impl.supported !== null) { return impl.supported; } // check for getEntriesByType and the entry type existing var p = BOOMR.getPerformance(); impl.supported = p && typeof BOOMR.window.PerformancePaintTiming !== "undefined" && typeof p.getEntriesByType === "function"; return impl.supported; }, /** * Gets the PaintTiming timestamp for the specified name * * @param {string} timingName PaintTiming name * * @returns {DOMHighResTimestamp} Timestamp * @memberof BOOMR.plugins.PaintTiming */ getTimingFor: function(timingName) { var p, paintTimings, i; // look in our cache first if (impl.timingCache[timingName]) { return impl.timingCache[timingName]; } // skip if not supported if (!this.is_supported()) { return; } // need to get the window.performance interface var p = BOOMR.getPerformance(); if (!p || typeof p.getEntriesByType !== "function") { return; } // get all Paint Timings paintTimings = p.getEntriesByType("paint"); if (paintTimings && paintTimings.length) { for (i = 0; i < paintTimings.length; i++) { if (paintTimings[i].name === timingName) { // cache the value since it'll never change impl.timingCache[timingName] = paintTimings[i].startTime; return impl.timingCache[timingName]; } } } }, /* BEGIN_DEBUG */ /** * Get the history of timings for the specified metric */ getHistoryFor: function(timingName) { return impl.timingHistory[timingName] || []; }, /* END_DEBUG */ // external metrics metrics: { lcp: function() { return Math.floor(impl.lcp.time); }, lcpSrc: function() { return impl.lcp.src; }, lcpEl: function() { return impl.lcp.el; }, lcpId: function() { return impl.lcp.id; }, lcpE: function() { return impl.lcp.e; }, lcpSrcset: function() { return impl.lcp.srcset; }, lcpSizes: function() { return impl.lcp.sizes; }, lcpS: function() { return impl.lcp.s; } } }; }());