@inchill/time-to-interactive
Version:
Easily measure performance metrics of time to interactive in JavaScript.
309 lines (302 loc) • 9.85 kB
JavaScript
;
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _createForOfIteratorHelper(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (!it) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
var F = function () {};
return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = it.call(o);
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}
/**
* Entry type name of fcp.
*/
var ENTRY_NAME_FCP = 'first-contentful-paint';
/**
* Minimum quiet window duration(At least 5 seconds).
*/
var QUIET_WINDOW_DURATION = 5000;
/**
* TTI scores.
*/
var RATING = {
mobile: {
scoring: {
p10: 3785,
median: 7300
}
},
desktop: {
scoring: {
p10: 2468,
median: 4500
}
}
};
/**
* Performantly generate a unique, 30-char string by combining a version
* number, the current timestamp with a 13-digit number integer.
* @return {string}
*/
var generateUniqueID = function generateUniqueID() {
return "v3-".concat(Date.now(), "-").concat(Math.floor(Math.random() * (9e12 - 1)) + 1e12);
};
var initMetric = function initMetric(name, value) {
return {
name: name,
value: typeof value === 'undefined' ? -1 : value,
rating: getRating(0),
id: generateUniqueID()
};
};
/**
*
* @param value number
* @returns 'good' | 'needs-improvement' | 'poor'
*/
var getRating = function getRating(value) {
if (value > RATING.mobile.scoring.median) {
return 'poor';
}
if (value > RATING.mobile.scoring.p10) {
return 'needs-improvement';
}
return 'good';
};
/**
* Check if is's Safari browser.
* @returns boolean
*/
function isSafari() {
return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
}
var uniqueId = 0;
/**
* Override the open and send methods of XMLHttpRequest to track XHR requests
* @param beforeRequestCb function
* @param onRequestCompletedCb function
*/
var patchXMLHTTPRequest = function patchXMLHTTPRequest(beforeRequestCb, onRequestCompletedCb) {
var open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function () {
var requestId = uniqueId++;
beforeRequestCb(requestId);
this.addEventListener('loadend', function () {
onRequestCompletedCb(requestId);
});
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
open.apply(this, [].concat(args));
};
};
/**
* Override the Fetch function to track Fetch requests
* @param beforeRequestCb function
* @param onRequestCompletedCb function
*/
var patchFetch = function patchFetch(beforeRequestCb, onRequestCompletedCb) {
var fetch = window.fetch;
window.fetch = function () {
var requestId = uniqueId++;
beforeRequestCb(requestId);
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return fetch.apply(this, [].concat(args)).then(function (response) {
onRequestCompletedCb(requestId);
return response;
});
};
};
/**
* Calculates the [TTI](https://web.dev/tti/) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `paint` performance entry used to determine the value. The reported
* value is a `DOMHighResTimeStamp`.
*/
var onTTI = function onTTI(onReport) {
if (isSafari()) return;
var fcpStartTime = 0;
var longTasks = [];
var networks = [];
var quietWindowTimer;
var metric = initMetric('TTI');
var inProgressRequestStartTimes = new Map();
/**
* Mark active network requests.
* @param requestId number
*/
var beforeRequestCallback = function beforeRequestCallback(requestId) {
inProgressRequestStartTimes.set(requestId, performance.now());
};
/**
* Remove finished network requests.
* @param requestId number
*/
var afterRequestCallback = function afterRequestCallback(requestId) {
inProgressRequestStartTimes["delete"](requestId);
};
// Patch XHR and Fetch API to detect in-flight network requests.
patchXMLHTTPRequest(beforeRequestCallback, afterRequestCallback);
patchFetch(beforeRequestCallback, afterRequestCallback);
/**
* Track longtask and networks and check quiet window.
* @param entries PerformanceObserverEntryList
*/
var handleEntries = function handleEntries(entries) {
var _iterator = _createForOfIteratorHelper(entries.getEntries()),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var entry = _step.value;
if (entry.entryType === 'paint' && entry.name === ENTRY_NAME_FCP) {
fcpStartTime = entry.startTime;
}
// Longtasks could not be detected in Safari.
if (entry.entryType === 'longtask') {
longTasks.push(entry);
checkQuietWindow();
}
if (entry.entryType === 'resource') {
networks.push(entry);
checkQuietWindow();
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
};
/**
* Register to observe.
*/
var observer = new PerformanceObserver(handleEntries);
observer.observe({
entryTypes: ['paint', 'longtask', 'resource']
});
// There are some cases need to be handled.
// 1. There are no long tasks after fcp but there are network requests.
// 2. There are neither long tasks nor network requests after fcp
// In such case we need onload event to handle tti.
// 3. After fcp, there are both long tasks and network requests.
// 4. There are long tasks after fcp but no network requests.
var checkQuietWindow = function checkQuietWindow() {
// Filter longtasks after fcp.
var longTasksAfterFcp = longTasks.filter(function (item) {
return item.startTime >= fcpStartTime;
});
// If there are no longtasks after fcp, start the quiet window timer
// and count the number of in-flight network requests for at least 5 seconds.
if (longTasksAfterFcp.length === 0) {
startQuietWindowTimer(fcpStartTime);
} else {
// Get the last longtask.
var lastLongTask = longTasksAfterFcp[longTasksAfterFcp.length - 1];
// Turn on quiet window detection using the latest long task end time as the start time.
startQuietWindowTimer(lastLongTask.startTime + lastLongTask.duration);
}
};
/**
* Start quiet window detection timer.
*/
function startQuietWindowTimer(startTime) {
// Clear the last detection timer.
clearQuietWindowTimer();
quietWindowTimer = window.setTimeout(function () {
checkTTI(startTime);
}, QUIET_WINDOW_DURATION);
}
var checkTTI = function checkTTI(startTime) {
// Count the number of in-flight network requests in 5 seconds.
var inProgressRequestStarts = _toConsumableArray(inProgressRequestStartTimes.values());
// If the number of network requests is less than or equal to 2,
// find the nearest long task before the quiet window.
if (inProgressRequestStarts.length <= 2) {
// If no longtask is found, the value of TTI is equal to FCP.
// If find the nearest long task before the quiet window, and
// its endTime is equal to TTI.
metric.value = startTime;
// Set the rating value based on the obtained tti time.
metric.rating = getRating(metric.value);
// Call the onReport callback if the quiet window was found.
onReport(metric);
// After reporting disconnect the PerformanceObserver.
observer.disconnect();
clearQuietWindowTimer();
}
};
// Clear timer.
function clearQuietWindowTimer() {
clearTimeout(quietWindowTimer);
}
};
exports.onTTI = onTTI;