Created
May 24, 2025 04:21
-
-
Save glaszig/c9558776dc8857d4b476a81918cd5685 to your computer and use it in GitHub Desktop.
peek with vanilla js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://github.com/peek/peek-performance_bar/blob/c7e3d1b6f93ad054e8770e261b9e20aca881bb45/app/assets/javascripts/peek/views/performance_bar.js | |
// The mission control window.performance.timing display area. | |
// | |
// Breaks the window.performance.timing numbers down into these groups: | |
// | |
// dns - Time looking up domain. Usually zero. | |
// tcp and ssl - Time used establishing tcp and ssl connections. | |
// redirect - Time spent redirecting since navigation began. | |
// app - Real server time as recorded in the app. | |
// latency - Extra backend and network time where browser was waiting. | |
// frontend - Time spent loading and rendering the DOM until interactive. | |
// | |
// Not all frontend time is counted. The page is considered ready when the | |
// domInteractive ready state is reached. This is before DOMContentLoaded and | |
// onload javascript handlers. | |
class PerformanceBar { | |
static initClass() { | |
// Additional app info to show with the app timing. | |
this.prototype.appInfo = null | |
// The pixel width we're rendering the timing graph into. | |
this.prototype.width = null | |
} | |
// Format a time as ms or s based on how big it is. | |
static formatTime(value) { | |
if (value >= 1000) { | |
return `${(value / 1000).toFixed(3)}s` | |
} | |
return `${value.toFixed(0)}ms` | |
} | |
// Create a new PerformanceBar view bound to a given element. The el and width | |
// options should be provided here. | |
constructor(options) { | |
this.el = document.querySelector('#peek-view-performance-bar .performance-bar') | |
options = options || {} | |
Object.entries(options).forEach(([ key, value ]) => { | |
this[key] = value | |
}) | |
if (this.width == null) { this.width = this.el.getBoundingClientRect().width } | |
if (this.timing == null) { this.timing = window.performance.timing } | |
} | |
reset() { | |
while (this.el.firstChild) { | |
this.el.removeChild(this.el.firstChild) | |
} | |
} | |
// Render the performance bar in the associated element. This is a little weird | |
// because it includes the server-side rendering time reported with the | |
// response document which may not line up when using the back/forward button | |
// and loading from cache. | |
render(serverTime) { | |
if (serverTime == null) { serverTime = 0 } | |
this.reset() | |
this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive') | |
// time spent talking with the app according to performance.timing | |
const perfNetworkTime = (this.timing.responseEnd - this.timing.requestStart) | |
// only include serverTime if it's less than than the browser reported | |
// talking-to-the-app time; otherwise, assume we're loading from cache. | |
if (serverTime && (serverTime <= perfNetworkTime)) { | |
const networkTime = perfNetworkTime - serverTime | |
this.addBar( | |
'latency / receiving', | |
'#f1faff', | |
this.timing.requestStart + serverTime, | |
this.timing.requestStart + serverTime + networkTime | |
) | |
this.addBar( | |
'app', | |
'#90afcf', | |
this.timing.requestStart, | |
this.timing.requestStart + serverTime, | |
this.appInfo | |
) | |
} else { | |
this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd') | |
} | |
this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd') | |
this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd') | |
this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd') | |
return this.el | |
} | |
// Determine if the page has reached the interactive state yet. | |
isLoaded() { | |
return this.timing.domInteractive | |
} | |
// Integer unix timestamp representing the very beginning of the graph. | |
start() { | |
return this.timing.navigationStart | |
} | |
// Integer unix timestamp representing the very end of the graph. | |
end() { | |
return this.timing.domInteractive | |
} | |
// Total number of milliseconds between the start and end times. | |
total() { | |
return this.end() - this.start() | |
} | |
// Helper used to add a bar to the graph. | |
addBar(name, color, start, end) { | |
if (typeof start === 'string') { start = this.timing[start] } | |
if (typeof end === 'string') { end = this.timing[end] } | |
// Skip missing stats | |
if ((start == null) || (end == null)) { return } | |
const time = end - start | |
const offset = start - this.start() | |
const left = this.mapH(offset) | |
const width = this.mapH(time) | |
const title = `${name}: ${PerformanceBar.formatTime(time)}` | |
const bar = document.createElement('li') | |
bar.title = title | |
bar.style = `width:${width}px;left:${left}px;background:${color};` | |
this.el.appendChild(bar) | |
} | |
// Map a time offset value to a horizontal pixel offset. | |
mapH(offset) { | |
return offset * (this.width / this.total()) | |
} | |
} | |
PerformanceBar.initClass() | |
function updateStatus(value, title) { | |
const span = document.createElement('span') | |
span.title = title | |
span.textContent = value | |
document.getElementById('serverstats').innerHTML = '' | |
document.getElementById('serverstats').append(span) | |
} | |
function renderPerformanceBar() { | |
const bar = new PerformanceBar() | |
bar.render(0) | |
updateStatus(PerformanceBar.formatTime(bar.total()), 'Total navigation time for this page.') | |
} | |
let ajaxStart = null | |
function onAjaxStart(event) { | |
ajaxStart = event.timeStamp | |
} | |
document.addEventListener('page:fetch', onAjaxStart) | |
document.addEventListener('turbolinks:request-start', onAjaxStart) | |
document.addEventListener('turbo:before-visit', onAjaxStart) | |
function onLoad(event) { | |
if (ajaxStart == null) { return } | |
const ajaxEnd = event.timeStamp | |
const total = ajaxEnd - ajaxStart | |
const timing = { | |
requestStart: ajaxStart, | |
responseEnd: ajaxEnd, | |
domLoading: ajaxEnd | |
} | |
setTimeout( | |
() => { | |
const now = performance.now() | |
const bar = new PerformanceBar({ | |
timing: { ...timing, domInteractive: now }, | |
isLoaded() { return true }, | |
start() { return ajaxStart }, | |
end() { return now } | |
}) | |
bar.render(0) | |
let tech | |
if (window.Turbo && window.Turbo.visit) { | |
tech = 'Turbo' | |
} else { | |
tech = 'Turbolinks' | |
} | |
updateStatus(PerformanceBar.formatTime(total), `${tech} navigation time`) | |
ajaxStart = null | |
}, | |
0 | |
) | |
} | |
document.addEventListener('page:load', onLoad) | |
document.addEventListener('turbolinks:load', onLoad) | |
document.addEventListener('turbo:load', onLoad) | |
document.addEventListener('DOMContentLoaded', () => { | |
if (window.performance) { | |
renderPerformanceBar() | |
} else { | |
document.getElementById('peek-view-performance-bar').remove() | |
} | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js | |
(function init() { | |
let requestId | |
function trigger(el, ev, dt) { | |
el.dispatchEvent(new CustomEvent(ev, { detail: dt })) | |
} | |
function isInputElement(el) { | |
return el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA' | |
} | |
function getRequestId() { | |
return requestId || document.getElementById('peek').dataset.requestId | |
} | |
function peekEnabled() { | |
return !!document.getElementById('peek') | |
} | |
function updatePerformanceBar(results) { | |
Object.entries(results.data).forEach(([ key, labels ]) => { | |
Object.entries(labels).forEach(([ label, value ]) => { | |
document.querySelectorAll(`[data-defer-to=${key}-${label}]`).forEach(el => { | |
el.textContent = value | |
}) | |
}) | |
}) | |
trigger(document, 'peek:render', { requestId: getRequestId(), results: results }) | |
} | |
function toggleBar(event) { | |
if (isInputElement(event.target)) { | |
return | |
} | |
if (event.key === '0' && !event.metaKey) { | |
const wrapper = document.getElementById('peek') | |
if (wrapper.classList.contains('disabled')) { | |
wrapper.classList.remove('disabled') | |
document.cookie = "peek=true; path=/" | |
} else { | |
wrapper.classList.add('disabled') | |
document.cookie = "peek=false; path=/" | |
} | |
} | |
} | |
function fetchRequestResults() { | |
const headers = { "X-Requested-With": "XMLHttpRequest" } | |
const query = "request_id=" + encodeURIComponent(getRequestId()) | |
fetch("/peek/results?" + query, { headers: headers }) | |
.then(r => r.json()) | |
.then(updatePerformanceBar) | |
.catch(() => {}) | |
} | |
function updatePeek() { | |
if (peekEnabled()) { | |
trigger(document, 'peek:update') | |
} | |
} | |
document.addEventListener('keypress', toggleBar) | |
document.addEventListener('peek:update', fetchRequestResults) | |
document.addEventListener('page:change', updatePeek) | |
document.addEventListener('turbolinks:load', updatePeek) | |
document.addEventListener('DOMContentLoaded', updatePeek) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment