split-time
Version:
A JavaScript library for measuring FCP, LCP. Report real user measurements to tracking tool.
346 lines (285 loc) • 7.87 kB
JavaScript
/* global MutationObserver, performance */
const utils = {
// 获取当前样式
getStyle (element, att) {
return window.getComputedStyle(element)[att]
}
}
const TAG_WEIGHT_MAP = {
SVG: 2,
IMG: 2,
CANVAS: 4,
OBJECT: 4,
EMBED: 4,
VIDEO: 4
}
const WW = window.innerWidth
const WH = window.innerHeight
// const VIEWPORT_AREA = WW * WH
/**
* Class to detect metrics.
*/
export default class Detector {
/**
* @param {!DetectorInit=} config
*/
constructor (config = {}) {
this.endpoints = []
this.isChecking = false
this._mutationCount = 1
this._timerId = null
// Timer tasks are only scheduled when detector is enabled.
this._scheduleTimerTasks = false
/** @type {?Function} */
this._firstMeaningfulPaintResolver = null
/** @type {?MutationObserver} */
this._mutationObserver = null
this.mp = {}
this._registerListeners()
}
/**
* Starts checking for a first meaningful time and returns a
* promise that resolves to the found time.
* @return {!Promise<number>}
*/
getFirstMeaningfulPaint () {
this._getSnapshot()
return new Promise((resolve, reject) => {
this._firstMeaningfulPaintResolver = resolve
if (document.readyState === 'complete') {
this.startSchedulingTimerTasks()
} else {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
this.startSchedulingTimerTasks()
}
}, true)
}
})
}
startSchedulingTimerTasks () {
this._scheduleTimerTasks = true
this.rescheduleTimer()
}
rescheduleTimer () {
const navigationStart = performance.timing.navigationStart
const DELAY = 500
if (!this._scheduleTimerTasks) {
console.log('startSchedulingTimerTasks must be called before calling rescheduleTimer')
return
}
if (!this.isChecking && this._ifRipe(navigationStart)) {
this._checkFMP()
} else {
clearTimeout(this._timerId)
this._timerId = setTimeout(() => {
this.rescheduleTimer()
}, DELAY)
}
}
deepTraversal (node) {
if (node) {
let dpss = []
for (let i = 0, child; (child = node.children[i]); i++) {
let s = this.deepTraversal(child)
if (s.st) {
dpss.push(s)
}
}
return this.calScore(node, dpss)
}
return {}
}
calScore (node, dpss) {
let {
width,
height,
left,
top,
bottom,
right
} = node.getBoundingClientRect()
let f = 1
if (WH < top || WW < left) {
// 不在可视viewport中
f = 0
}
let sdp = 0
dpss.forEach(item => {
sdp += item.st
})
let weight = TAG_WEIGHT_MAP[node.tagName] || 1
if (
weight === 1 &&
utils.getStyle(node, 'background-image') &&
utils.getStyle(node, 'background-image') !== 'initial' &&
utils.getStyle(node, 'background-image') !== 'none'
) {
weight = TAG_WEIGHT_MAP['IMG'] // 将有图片背景的普通元素 权重设置为img
}
let st = width * height * weight * f
let els = [{ node, st, weight }]
let areaPercent = this.calAreaPercent(node)
if (sdp > st * areaPercent || areaPercent === 0) {
st = sdp
els = []
dpss.forEach(item => {
els = els.concat(item.els)
})
}
return {
dpss,
st,
els
}
}
calAreaPercent (node) {
let {
left,
right,
top,
bottom,
width,
height
} = node.getBoundingClientRect()
let wl = 0
let wt = 0
let wr = WW
let wb = WH
let overlapX =
right - left + (wr - wl) - (Math.max(right, wr) - Math.min(left, wl))
if (overlapX <= 0) {
// x 轴无交点
return 0
}
let overlapY =
bottom - top + (wb - wt) - (Math.max(bottom, wb) - Math.min(top, wt))
if (overlapY <= 0) {
return 0
}
return (overlapX * overlapY) / (width * height)
}
filterTheResultSet (els) {
let sum = 0
els.forEach(item => {
sum += item.st
})
let avg = sum / els.length
return els.filter(item => {
return item.st > avg
})
}
initResourceMap () {
performance.getEntries().forEach(item => {
this.mp[item.name] = item.responseEnd
})
}
calResult (resultSet) {
let rt = 0
resultSet.forEach(item => {
let t = 0
if (item.weight === 1) {
let index = +item.node.getAttribute('mr_c') - 1
t = this.endpoints[index].timestamp
} else if (item.weight === 2) {
if (item.node.tagName === 'IMG') {
t = this.mp[item.node.src]
} else if (item.node.tagName === 'SVG') {
let index = +item.node.getAttribute('mr_c') - 1
t = this.endpoints[index].timestamp
} else {
// background image
let match = utils.getStyle(item.node, 'background-image').match(/url\(\"(.*?)\"\)/)
let s
if (match && match[1]) {
s = match[1]
}
if (s.indexOf('http') == -1) {
s = location.protocol + match[1]
}
t = this.mp[s]
}
} else if (item.weight === 4) {
if (item.node.tagName === 'CANVAS') {
let index = +item.node.getAttribute('mr_c') - 1
t = this.endpoints[index].timestamp
} else if (item.node.tagName === 'VIDEO') {
t = this.mp[item.node.src]
!t && (t = this.mp[item.node.poster])
}
}
console.log(t, item.node)
rt < t && (rt = t)
})
return rt
}
disable () {
clearTimeout(this._timerId)
this._scheduleTimerTasks = false
this._unregisterListeners()
}
_ifRipe (start) {
const LIMIT = 1000
const ti = Date.now() - start
return ti > LIMIT || ti - (this.endpoints[this.endpoints.length - 1].timestamp) > 1000
}
_checkFMP () {
this.isChecking = true
let res = this.deepTraversal(document.body)
let tp
res.dpss.forEach(item => {
if (tp && tp.st) {
if (tp.st < item.st) {
tp = item
}
} else {
tp = item
}
})
this.initResourceMap()
let resultSet = this.filterTheResultSet(tp.els)
let fmpTiming = this.calResult(resultSet)
this._firstMeaningfulPaintResolver(fmpTiming)
this.disable()
console.log(tp, this.endpoints)
}
_setMutationRecord (target, mutationCount) {
const tagName = target.tagName
const IGNORE_TAG_SET = new Set(['META', 'LINK', 'STYLE', 'SCRIPT', 'NOSCRIPT'])
if (!IGNORE_TAG_SET.has(tagName) && target.children) {
for (let child = target.children, i = target.children.length - 1; i >= 0; i--) {
if (child[i].getAttribute('mr_c') === null) {
child[i].setAttribute('mr_c', mutationCount)
}
this._setMutationRecord(child[i], mutationCount)
}
}
}
_getSnapshot () {
const navigationStart = performance.timing.navigationStart
const timestamp = Date.now() - navigationStart
this._setMutationRecord(document, this._mutationCount++)
this.endpoints.push({ timestamp })
}
/**
* Registers listeners to detect DOM mutations to detect mutation and network quiescence.
*/
_registerListeners () {
this._mutationObserver = new MutationObserver((mutations) => {
// Typecast to fix: https://github.com/google/closure-compiler/issues/2539
// eslint-disable-next-line no-self-assign
mutations = /** @type {!Array<!MutationRecord>} */ (mutations)
this._getSnapshot()
})
this._mutationObserver.observe(document, {
childList: true,
subtree: true
})
}
/**
* Removes all added listeners.
*/
_unregisterListeners () {
if (this._mutationObserver) this._mutationObserver.disconnect()
}
}