Nemo Metric(check the sourceCode)主要分爲四個模塊:javascript
performance:主要是performance以及performanceObserver的一些調用的封裝。java
detect-browser:用於檢測瀏覽器的名字,版本,以及操做系統。node
idle-queue: 實現將任務放入隊列,在cpu空閒時候才執行,在這裏就是檢測到指標數據之後丟到這個隊列裏面讓它來統一處理。android
Nemetric: 供外部調用的類,接受指標參數,採樣率,指標檢測回調等參數而後調用detect-browser,Idle-queue以及performance實現對性能的採集。ios
利用Performance 的Performance Timeline API, Navigation Timing API, User Timing API,Resource Timing API獲取Navigation指標,資源指標以及用戶動做時間指標等,利用PerformanceObserver監聽firstPaint,firstContentfulPaint,firstInputDelay等。git
首先是判斷方法的支持性,不支持就沒辦法了。github
static supported(): boolean { return ( window.performance && !!performance.getEntriesByType && !!performance.now && !!performance.mark ); } static supportedPerformanceObserver(): boolean { return (window as any).chrome && 'PerformanceObserver' in window; } 複製代碼
直接 performance.getEntriesByType('navigation')[0] 獲取到Navigation 這個Entry,而後再得到相應的指標便可。web
export interface INemetricNavigationTiming { fetchTime?: number; workerTime?: number; totalTime?: number; downloadTime?: number; timeToFirstByte?: number; headerSize?: number; dnsLookupTime?: number; } /** * Navigation Timing API provides performance metrics for HTML documents. * w3c.github.io/navigation-timing/ * developers.google.com/web/fundamentals/performance/navigation-and-resource-timing */ get navigationTiming(): INemetricNavigationTiming { if ( !Performance.supported() || Object.keys(this.navigationTimingCached).length ) { return this.navigationTimingCached; } // There is an open issue to type correctly getEntriesByType // github.com/microsoft/TypeScript/issues/33866 const navigation = performance.getEntriesByType('navigation')[0] as any; // In Safari version 11.2 Navigation Timing isn't supported yet if (!navigation) { return this.navigationTimingCached; } // We cache the navigation time for future times this.navigationTimingCached = { // fetchStart marks when the browser starts to fetch a resource // responseEnd is when the last byte of the response arrives fetchTime: parseFloat((navigation.responseEnd - navigation.fetchStart).toFixed(2)), // Service worker time plus response time workerTime: parseFloat( (navigation.workerStart > 0 ? navigation.responseEnd - navigation.workerStart : 0).toFixed(2), ), // Request plus response time (network only) totalTime: parseFloat((navigation.responseEnd - navigation.requestStart).toFixed(2)), // Response time only (download) downloadTime: parseFloat((navigation.responseEnd - navigation.responseStart).toFixed(2)), // Time to First Byte (TTFB) timeToFirstByte: parseFloat( (navigation.responseStart - navigation.requestStart).toFixed(2), ), // HTTP header size headerSize: parseFloat((navigation.transferSize - navigation.encodedBodySize).toFixed(2)), // Measuring DNS lookup time dnsLookupTime: parseFloat( (navigation.domainLookupEnd - navigation.domainLookupStart).toFixed(2), ), }; return this.navigationTimingCached; } 複製代碼
主要是利用Performance.mark以及Performance.measure方法,核心就是對一個metric記錄兩遍,而後調用measure 獲取獲得duration。chrome
mark(metricName: string, type: string): void { const mark = `mark_${metricName}_${type}`; (window.performance.mark as any)(mark); } measure(metricName: string, metric: IMetricEntry): number { const startMark = `mark_${metricName}_start`; const endMark = `mark_${metricName}_end`; (window.performance.measure as any)(metricName, startMark, endMark); return this.getDurationByMetric(metricName, metric); } 複製代碼
Nemetric對其進行封裝對外暴露 start和 end 方法,而endPaint是一些UI渲染以及異步操做的記錄。數據庫
/** * Start performance measurement */ start(metricName: string): void { if (!this.checkMetricName(metricName) || !Performance.supported()) { return; } if (this.metrics[metricName]) { this.logWarn('Recording already started.'); return; } this.metrics[metricName] = { end: 0, start: this.perf.now(), }; // Creates a timestamp in the browser's performance entry buffer this.perf.mark(metricName, 'start'); // Reset hidden value this.isHidden = false; } /** * End performance measurement */ end(metricName: string): void | number { if (!this.checkMetricName(metricName) || !Performance.supported()) { return; } const metric = this.metrics[metricName]; if (!metric) { this.logWarn('Recording already stopped.'); return; } // End Performance Mark metric.end = this.perf.now(); this.perf.mark(metricName, 'end'); // Get duration and change it to a two decimal value const duration = this.perf.measure(metricName, metric); const duration2Decimal = parseFloat(duration.toFixed(2)); delete this.metrics[metricName]; this.pushTask(() => { // Log to console, delete metric and send to analytics tracker this.log({ metricName, duration: duration2Decimal }); this.sendTiming({ metricName, duration: duration2Decimal }); }); return duration2Decimal; } /** * End performance measurement after first paint from the beging of it */ endPaint(metricName: string): Promise<void | number> { return new Promise(resolve => { setTimeout(() => { const duration = this.end(metricName); resolve(duration); }); }); } 複製代碼
Performance提供方法給Nemetric監聽某個EventType.
/** * PerformanceObserver subscribes to performance events as they happen * and respond to them asynchronously. */ performanceObserver( eventType: IPerformanceObserverType, cb: (entries: any[]) => void, ): IPerformanceObserver { this.perfObserver = new PerformanceObserver( this.performanceObserverCb.bind(this, cb), ); // Retrieve buffered events and subscribe to newer events for Paint Timing this.perfObserver.observe({ type: eventType, buffered: true }); return this.perfObserver; } private performanceObserverCb( cb: (entries: PerformanceEntry[]) => void, entryList: IPerformanceObserverEntryList, ): void { const entries = entryList.getEntries(); cb(entries); } 複製代碼
Nemetric 根據調用參數來初始化須要監聽的指標,包括(firstPaint,firstContentfulPaint,firstInputDelay,dataConsumption)
private initPerformanceObserver(): void { // Init observe FCP and creates the Promise to observe metric if (this.config.firstPaint || this.config.firstContentfulPaint) { this.observeFirstPaint = new Promise(resolve => { this.logDebug('observeFirstPaint'); this.observers['firstPaint'] = resolve; }); this.observeFirstContentfulPaint = new Promise(resolve => { this.logDebug('observeFirstContentfulPaint'); this.observers['firstContentfulPaint'] = resolve; this.initFirstPaint(); }); } // FID needs to be initialized as soon as Nemetric is available, // which returns a Promise that can be observed. // DataConsumption resolves after FID is triggered this.observeFirstInputDelay = new Promise(resolve => { this.observers['firstInputDelay'] = resolve; this.initFirstInputDelay(); }); // Collects KB information related to resources on the page if (this.config.dataConsumption) { this.observeDataConsumption = new Promise(resolve => { this.observers['dataConsumption'] = resolve; this.initDataConsumption(); }); } } 複製代碼
原理都是同樣,初始化每一個指標,對他們進行PerformanceObserver.observe就行監聽,等到監聽有結果就調用digest函數,digest統一調用performanceObserverCb.而performanceObserverCb就是咱們整個代碼的核心!
private performanceObserverResourceCb(options: { entries: IPerformanceEntry[]; }): void { this.logDebug('performanceObserverResourceCb', options); options.entries.forEach((performanceEntry: IPerformanceEntry) => { if (performanceEntry.decodedBodySize) { const decodedBodySize = parseFloat( (performanceEntry.decodedBodySize / 1000).toFixed(2), ); this.dataConsumption += decodedBodySize; } }); } private digestFirstPaintEntries(entries: IPerformanceEntry[]): void { this.performanceObserverCb({ entries, entryName: 'first-paint', metricLog: 'First Paint', metricName: 'firstPaint', valueLog: 'startTime', }); this.performanceObserverCb({ entries, entryName: 'first-contentful-paint', metricLog: 'First Contentful Paint', metricName: 'firstContentfulPaint', valueLog: 'startTime', }); } /** * First Paint is essentially the paint after which * the biggest above-the-fold layout change has happened. */ private initFirstPaint(): void { this.logDebug('initFirstPaint'); try { this.perfObservers.firstContentfulPaint = this.perf.performanceObserver( 'paint', this.digestFirstPaintEntries.bind(this), ); } catch (e) { this.logWarn('initFirstPaint failed'); } } private digestFirstInputDelayEntries(entries: IPerformanceEntry[]): void { this.performanceObserverCb({ entries, metricLog: 'First Input Delay', metricName: 'firstInputDelay', valueLog: 'duration', }); this.disconnectDataConsumption(); } private initFirstInputDelay(): void { try { this.perfObservers.firstInputDelay = this.perf.performanceObserver( 'first-input', this.digestFirstInputDelayEntries.bind(this), ); } catch (e) { this.logWarn('initFirstInputDelay failed'); } } private digestDataConsumptionEntries(entries: IPerformanceEntry[]): void { this.performanceObserverResourceCb({ entries, }); } private disconnectDataConsumption(): void { clearTimeout(this.dataConsumptionTimeout); if (!this.perfObservers.dataConsumption || !this.dataConsumption) { return; } this.logMetric( this.dataConsumption, 'Data Consumption', 'dataConsumption', 'Kb', ); this.perfObservers.dataConsumption.disconnect(); } private initDataConsumption(): void { try { this.perfObservers.dataConsumption = this.perf.performanceObserver( 'resource', this.digestDataConsumptionEntries.bind(this), ); } catch (e) { this.logWarn('initDataConsumption failed'); } this.dataConsumptionTimeout = setTimeout(() => { this.disconnectDataConsumption(); }, 15000); } 複製代碼
performanceObserverCb 接受一個指標的參數,而後找到對應的EntryName,調用PushTask將任務放到Idle-queue裏面。而任務就是logMetric
private pushTask(cb: any): void { if (this.queue && this.queue.pushTask) { this.queue.pushTask(() => { cb(); }); } else { cb(); } } /** * Logging Performance Paint Timing */ private performanceObserverCb(options: { entries: IPerformanceEntry[]; entryName?: string; metricLog: string; metricName: INemetricMetrics; valueLog: 'duration' | 'startTime'; }): void { this.logDebug('performanceObserverCb', options); options.entries.forEach((performanceEntry: IPerformanceEntry) => { this.pushTask(() => { if ( this.config[options.metricName] && (!options.entryName || (options.entryName && performanceEntry.name === options.entryName)) ) { this.logMetric( performanceEntry[options.valueLog], options.metricLog, options.metricName, ); } }); if ( this.perfObservers.firstContentfulPaint && performanceEntry.name === 'first-contentful-paint' ) { this.perfObservers.firstContentfulPaint.disconnect(); } }); if ( this.perfObservers.firstInputDelay && options.metricName === 'firstInputDelay' ) { this.perfObservers.firstInputDelay.disconnect(); } } 複製代碼
logMetric很簡單,就是調用輸出log(此代碼就不貼出來了),以及sendtiming,sendtiming就是用戶傳參給**Nemetric **的analyticsTracker分析結果動做回調函數。
/** * Dispatches the metric duration into internal logs * and the external time tracking service. */ private logMetric( duration: number, logText: string, metricName: string, suffix: string = 'ms', ): void { const duration2Decimal = parseFloat(duration.toFixed(2)); // Stop Analytics and Logging for false negative metrics if ( metricName !== 'dataConsumption' && duration2Decimal > this.config.maxMeasureTime ) { return; } else if ( metricName === 'dataConsumption' && duration2Decimal > this.config.maxDataConsumption ) { return; } // Save metrics in Duration property if (metricName === 'firstPaint') { this.firstPaintDuration = duration2Decimal; } if (metricName === 'firstContentfulPaint') { this.firstContentfulPaintDuration = duration2Decimal; } if (metricName === 'firstInputDelay') { this.firstInputDelayDuration = duration2Decimal; } this.observers[metricName](duration2Decimal); // Logs the metric in the internal console.log this.log({ metricName: logText, duration: duration2Decimal, suffix }); // Sends the metric to an external tracking service this.sendTiming({ metricName, duration: duration2Decimal }); } sendTiming(options: ISendTimingOptions): void { const { metricName, data, duration } = options; // Doesn't send timing when page is hidden if (this.isHidden) { return; } // Get Browser from userAgent const browser = this.browser; // Send metric to custom Analytics service, if (this.config.analyticsTracker) { //random track Math.random() < this.config.sampleRate && this.config.analyticsTracker({ data, metricName, duration, browser }); } } 複製代碼
detect-browser 其實很簡單,就是根據userAgent,對Nemetric暴露detect方法,而後主要是parseUserAgent枚舉匹配對得上的userAgentRules.
type Browser =
| 'welike'
| 'vidmate'
| 'aol'
| 'edge'
| 'yandexbrowser'
| 'vivaldi'
| 'kakaotalk'
| 'samsung'
| 'chrome'
| 'phantomjs'
| 'crios'
| 'firefox'
| 'fxios'
| 'opera'
| 'ie'
| 'bb10'
| 'android'
| 'ios'
| 'safari'
| 'facebook'
| 'instagram'
| 'ios-webview' 
| 'searchbot';
type OperatingSystem =
| 'iOS'
| 'Android OS'
| 'BlackBerry OS'
| 'Windows Mobile'
| 'Amazon OS'
| 'Windows 3.11'
| 'Windows 95'
| 'Windows 98'
| 'Windows 2000'
| 'Windows XP'
| 'Windows Server 2003'
| 'Windows Vista'
| 'Windows 7'
| 'Windows 8'
| 'Windows 8.1'
| 'Windows 10'
| 'Windows ME'
| 'Open BSD'
| 'Sun OS'
| 'Linux'
| 'Mac OS'
| 'QNX'
| 'BeOS'
| 'OS/2'
| 'Search Bot';
const userAgentRules: UserAgentRule[] = [
['aol', /AOLShield\/([0-9\._]+)/],
['edge', /Edge\/([0-9\._]+)/],
['yandexbrowser', /YaBrowser\/([0-9\._]+)/],
['vivaldi', /Vivaldi\/([0-9\.]+)/],
['kakaotalk', /KAKAOTALK\s([0-9\.]+)/],
['samsung', /SamsungBrowser\/([0-9\.]+)/],
['chrome', /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],
['phantomjs', /PhantomJS\/([0-9\.]+)(:?\s|$)/],
['crios', /CriOS\/([0-9\.]+)(:?\s|$)/],
['firefox', /Firefox\/([0-9\.]+)(?:\s|$)/],
['fxios', /FxiOS\/([0-9\.]+)/],
['opera', /Opera\/([0-9\.]+)(?:\s|$)/],
['opera', /OPR\/([0-9\.]+)(:?\s|$)$/],
['ie', /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],
['ie', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],
['ie', /MSIE\s(7\.0)/],
['bb10', /BB10;\sTouch.*Version\/([0-9\.]+)/],
['android', /Android\s([0-9\.]+)/],
['ios', /Version\/([0-9\._]+).*Mobile.*Safari.*/],
['safari', /Version\/([0-9\._]+).*Safari/],
['facebook', /FBAV\/([0-9\.]+)/],
['instagram', /Instagram\s([0-9\.]+)/],
['ios-webview', /AppleWebKit\/([0-9\.]+).*Mobile/],
['searchbot', SEARCHBOX_UA_REGEX],
];
type UserAgentRule = [Browser, RegExp];
type UserAgentMatch = [Browser, RegExpExecArray] | false;
type OperatingSystemRule = [OperatingSystem, RegExp];
export function detect(): BrowserInfo | BotInfo | NodeInfo | null {
if (typeof navigator !== 'undefined') {
return parseUserAgent(navigator.userAgent);
}
return getNodeVersion();
}
export function parseUserAgent(ua: string): BrowserInfo | BotInfo | null {
// opted for using reduce here rather than Array#first with a regex.test call
// this is primarily because using the reduce we only perform the regex
// execution once rather than once for the test and for the exec again below
// probably something that needs to be benchmarked though
const matchedRule: UserAgentMatch =
ua !== '' &&
userAgentRules.reduce<UserAgentMatch>((matched: UserAgentMatch, [browser, regex]) => {
if (matched) {
return matched;
}
const uaMatch = regex.exec(ua);
return !!uaMatch && [browser, uaMatch];
}, false);
if (!matchedRule) {
return null;
}
const [name, match] = matchedRule;
if (name === 'searchbot') {
return new BotInfo();
}
let version = match[1] && match[1].split(/[._]/).slice(0, 3);
if (version) {
if (version.length < REQUIRED_VERSION_PARTS) {
version = [
...version,
...new Array(REQUIRED_VERSION_PARTS - version.length).fill('0'),
];
}
} else {
version = [];
}
return new BrowserInfo(name, version.join('.'), detectOS(ua));
}
複製代碼
這是谷歌大神Phlip Walton 給出的一個解決方案.Idle Queue維護一個任務隊列,在前面的Performance會看到,pushTask就是將任務放到這裏面,等到cpu空閒,任務纔開始執行。
pushTask(cb: any) { this.addTask_(Array.prototype.push, cb); } addTask_( arrayMethod: any, task: any, { minTaskTime = this.defaultMinTaskTime_ } = {}, ) { const state = { time: now(), visibilityState: document.visibilityState, }; arrayMethod.call(this.taskQueue_, { state, task, minTaskTime }); this.scheduleTasksToRun_(); } 複製代碼
核心就是 scheduleTasksToRun_,ensureTasksRun_是表示在頁面不可見時候任務是否繼續進行.
scheduleTasksToRun_() { if (this.ensureTasksRun_ && document.visibilityState === 'hidden') { queueMicrotask(this.runTasks_); } else { if (!this.idleCallbackHandle_) { this.idleCallbackHandle_ = rIC(this.runTasks_); } } } 複製代碼
其中 queueMictotask就是一個建立一個微任務,能夠看到,若是支持Promise就用promise,不然就用MutationObserver模擬一個微任務,若是MutationObserver都不支持的話,只能用同步代碼處理了。
/** * Queues a function to be run in the next microtask. If the browser supports * Promises, those are used. Otherwise it falls back to MutationObserver. * Note: since Promise polyfills are popular but not all support microtasks, * we check for native implementation rather than a polyfill. * @private * @param {!Function} microtask */ export const queueMicrotask = supportsPromisesNatively ? createQueueMicrotaskViaPromises() : supportsMutationObserver ? createQueueMicrotaskViaMutationObserver() : discardMicrotasks(); /** * @return {!Function} */ const createQueueMicrotaskViaPromises = () => { return (microtask: any) => { Promise.resolve().then(microtask); }; }; /** * @return {!Function} */ const createQueueMicrotaskViaMutationObserver = () => { let i = 0; let microtaskQueue: any = []; const observer = new MutationObserver(() => { microtaskQueue.forEach((microtask: any) => microtask()); microtaskQueue = []; }); const node = document.createTextNode(''); observer.observe(node, { characterData: true }); return (microtask: any) => { microtaskQueue.push(microtask); // Trigger a mutation observer callback, which is a microtask. // tslint:disable-next-line:no-increment-decrement node.data = String(++i % 2); }; }; const discardMicrotasks = () => { return (microtask: any) => {}; }; 複製代碼
而rIC就是 requestIdleCallBack的簡稱了,cIC 就是 cancelIdleCallBack,若是瀏覽器不支持requestIdleCallBack 和cancelIdleCallBack,就用setTimeout來代替。
export const rIC = supportsRequestIdleCallback_ ? window.requestIdleCallback : requestIdleCallbackShim; const requestIdleCallbackShim = (callback: any) => { const deadline = new IdleDealine(now()); return setTimeout(() => callback(deadline), 0); }; /** * The native `cancelIdleCallback()` function or `cancelIdleCallbackShim()` * if the browser doesn't support it. * @param {number} handle */ export const cIC = supportsRequestIdleCallback_ ? window.cancelIdleCallback : cancelIdleCallbackShim; /** * A minimal shim for the cancelIdleCallback function. This accepts a * handle identifying the idle callback to cancel. * @private * @param {number|null} handle */ const cancelIdleCallbackShim = (handle: any) => { clearTimeout(handle); }; /** * A minimal shim of the native IdleDeadline class. */ class IdleDealine { initTime_: any; /** @param {number} initTime */ constructor(initTime: any) { this.initTime_ = initTime; } /** @return {boolean} */ get didTimeout() { return false; } /** @return {number} */ timeRemaining() { return Math.max(0, 50 - (now() - this.initTime_)); } } 複製代碼
最最核心的就是runTasks了,就是在deadline前,不斷的處理任務的隊列,直到隊列爲空。
/** * Runs as many tasks in the queue as it can before reaching the * deadline. If no deadline is passed, it will run all tasks. * If an `IdleDeadline` object is passed (as is with `requestIdleCallback`) * then the tasks are run until there's no time remaining, at which point * we yield to input or other script and wait until the next idle time. * @param {IdleDeadline=} deadline * @private */ runTasks_(deadline?: any) { this.cancelScheduledRun_(); if (!this.isProcessing_) { this.isProcessing_ = true; // Process tasks until there's no time left or we need to yield to input. while ( this.hasPendingTasks() && !shouldYield(deadline, (this.taskQueue_[0] as any).minTaskTime) ) { const { task, state } = (this.taskQueue_ as any).shift(); this.state_ = state; task(state); this.state_ = null; } this.isProcessing_ = false; if (this.hasPendingTasks()) { // Schedule the rest of the tasks for the next idle time. this.scheduleTasksToRun_(); } } } /** * Returns true if the IdleDealine object exists and the remaining time is * less or equal to than the minTaskTime. Otherwise returns false. * @param {IdleDeadline|undefined} deadline * @param {number} minTaskTime * @return {boolean} * @private */ const shouldYield = (deadline: any, minTaskTime: any) => { if (deadline && deadline.timeRemaining() <= minTaskTime) { return true; } return false; }; /** * @return {boolean} */ hasPendingTasks() { return this.taskQueue_.length > 0; } 複製代碼
很是簡單了,對外暴露參數,而後根據參數調用相應模塊就行。
export interface INemetricOptions { // Metrics firstContentfulPaint?: boolean; firstInputDelay?: boolean; firstPaint?: boolean; dataConsumption?: boolean; navigationTiming?: boolean; // Analytics analyticsTracker?: (options: IAnalyticsTrackerOptions) => void; // Logging logPrefix?: string; logging?: boolean; maxMeasureTime?: number; maxDataConsumption?: number; warning?: boolean; // Debugging debugging?: boolean; //is for in-app inApp?: boolean; //0~1 sampleRate?: number; } constructor(options: INemetricOptions = {}) { // Extend default config with external options this.config = Object.assign({}, this.config, options) as INemetricConfig; this.perf = new Performance(); // Exit from Nemetric when basic Web Performance APIs aren't supported if (!Performance.supported()) { return; } this.browser = detect(); // Checks if use Performance or the EmulatedPerformance instance if (Performance.supportedPerformanceObserver()) { this.initPerformanceObserver(); } // Init visibilitychange listener this.onVisibilityChange(); // /** * if it's built for in-App * or it's safari * also need to listen for beforeunload */ if (this.config.inApp || typeof window.safari === 'object' && window.safari.pushNotification) { this.onBeforeUnload(); } // Ensures the queue is run immediately whenever the page // is in a state where it might soon be unloaded. // https://philipwalton.com/articles/idle-until-urgent/ this.queue = new IdleQueue({ ensureTasksRun: true }); // Log Navigation Timing if (this.config.navigationTiming) { this.logNavigationTiming(); } } 複製代碼
Nemetric 主要是利用Performace以及Performace Observer來採集用戶的數據。正如 如何採集和分析網頁用戶的性能指標 所說,咱們算用戶指標的平均時長對咱們來講用處不大。利用這些數據咱們能夠