Skip to content

Instantly share code, notes, and snippets.

@nodaguti
Created November 14, 2019 06:04
Show Gist options
  • Save nodaguti/0ad99b83d9b7a5addbb99e5260f762ec to your computer and use it in GitHub Desktop.
Save nodaguti/0ad99b83d9b7a5addbb99e5260f762ec to your computer and use it in GitHub Desktop.
Resource Loading Performance Analyser
(() => {
// Based on:
// https://github.com/micmro/performance-bookmarklet
// MIT License Copyright (c) 2014 Michael Mrowetz
// https://github.com/micmro/performance-bookmarklet/blob/master/LICENSE
//extract a resources file type
const getFileType = (fileExtension, initiatorType) => {
if(fileExtension){
switch(fileExtension){
case "jpg" :
case "jpeg" :
case "png" :
case "gif" :
case "webp" :
case "svg" :
case "ico" :
return "image";
case "js" :
return "js"
case "css":
return "css"
case "html":
return "html"
case "woff":
case "woff2":
case "ttf":
case "eot":
case "otf":
return "font"
case "swf":
return "flash"
case "map":
return "source-map"
}
}
if(initiatorType){
switch(initiatorType){
case "xmlhttprequest" :
return "ajax"
case "img" :
return "image"
case "script" :
return "js"
case "internal" :
case "iframe" :
return "html" //actual page
default :
return "other"
}
}
return initiatorType;
};
const map = (arr, predicate) => {
const result = {};
arr.forEach((item) => {
const key = predicate(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
});
return result;
}
// --------------------------------------------------
const resources = window.performance.getEntriesByType("resource");
const marks = window.performance.getEntriesByType("mark");
marks.reduce((prev, curr) => {
if (!prev) return curr;
window.performance.measure(`${prev.name} - ${curr.name}`, prev.name, curr.name);
return curr;
}, null);
const measures = window.performance.getEntriesByType("measure");
const perfTiming = window.performance.timing;
const injectMetadataToEntry = (entry) => {
//crunch the resources data into something easier to work with
const isRequest = entry.name.startsWith("http");
let urlFragments, maybeFileName, fileExtension;
if(isRequest){
urlFragments = entry.name.match(/:\/\/(.[^/]+)([^?]*)\??(.*)/);
maybeFileName = urlFragments[2].split("/").pop();
fileExtension = maybeFileName.substr((Math.max(0, maybeFileName.lastIndexOf(".")) || Infinity) + 1);
}else{
urlFragments = ["", location.host];
fileExtension = entry.name.split(":")[0];
}
const currRes = {
name : entry.name,
domain : urlFragments[1],
initiatorType : entry.initiatorType || fileExtension || "SourceMap or Not Defined",
fileExtension : fileExtension || "XHR or Not Defined",
loadtime : entry.duration,
fileType : getFileType(fileExtension, entry.initiatorType),
isRequestToHost : urlFragments[1] === location.host
};
for(let attr in entry){
if(typeof entry[attr] !== "function") {
currRes[attr] = entry[attr];
}
}
if(entry.requestStart){
currRes.requestStartDelay = entry.requestStart - entry.startTime;
currRes.dns = entry.domainLookupEnd - entry.domainLookupStart;
currRes.tcp = entry.connectEnd - entry.connectStart;
currRes.ttfb = entry.responseStart - entry.startTime;
currRes.requestDuration = entry.responseStart - entry.requestStart;
}
if(entry.secureConnectionStart){
currRes.ssl = entry.connectEnd - entry.secureConnectionStart;
}
return currRes;
};
const getDurationParallelAndTotal = (entries) => {
let lastResponseEnd = 0;
let parallel = 0;
let total = 0;
entries.forEach((entry) => {
if (lastResponseEnd <= entry.startTime) {
parallel += entry.duration;
} else if (lastResponseEnd < entry.responseEnd) {
parallel += entry.responseEnd - lastResponseEnd;
}
total += entry.duration;
lastResponseEnd = entry.responseEnd;
});
return {
parallel,
total,
};
}
const getNearestMarkName = (entry) => {
const mark = marks.find((m) => m.startTime >= entry.startTime);
return mark ? mark.name : `After ${marks[marks.length - 1].name}`;
}
const resourcesWithMetadata = resources
//remove this bookmarklet from the result
.filter((currR) => !currR.name.match(/http[s]?\:\/\/(micmro|nurun).github.io\/performance-bookmarklet\/.*/))
.map(injectMetadataToEntry);
const requests = resourcesWithMetadata.filter((currR) => {
return currR.name.startsWith("http") && !currR.name.match(/js.map$/);
});
const requestsByMark = map(requests, getNearestMarkName);
let csv = [];
csv.push('mark, domain, parallel, total');
Object.entries(requestsByMark)
.forEach(([mark, entries]) => {
const requestsByDomain = map(entries, (e) => e.domain);
Object.entries(requestsByDomain)
.forEach(([domain, entries2]) => {
const { total, parallel } = getDurationParallelAndTotal(entries2);
csv.push(`${mark}, ${domain}, ${parallel}, ${total}`);
});
});
console.log(csv.join('\n'));
csv.push('mark, domain, url, duration');
csv.length = 0;
Object.entries(requestsByMark)
.forEach(([mark, entries]) => {
const requestsByDomain = map(entries, (e) => e.domain);
Object.entries(requestsByDomain)
.forEach(([domain, entries2]) => {
entries2.forEach((entry) => {
csv.push(`${mark}, ${domain}, ${entry.name}, ${entry.duration}`);
});
});
});
console.log(csv.join('\n'));
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment