Skip to content

Instantly share code, notes, and snippets.

@glaszig
Created May 24, 2025 04:21
Show Gist options
  • Save glaszig/c9558776dc8857d4b476a81918cd5685 to your computer and use it in GitHub Desktop.
Save glaszig/c9558776dc8857d4b476a81918cd5685 to your computer and use it in GitHub Desktop.
peek with vanilla js
// 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()
}
})
// 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