UNPKG

split-time

Version:

A JavaScript library for measuring FCP, LCP. Report real user measurements to tracking tool.

346 lines (285 loc) 7.87 kB
/* 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() } }