DIY一個前端性能採集系統(Nemo Metric實現原理)

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 的Performance Timeline APINavigation 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;
}

複製代碼

1. 獲取Navigation Timing(指標能夠繼續增長)

直接 performance.getEntriesByType('navigation')[0] 獲取到Navigation 這個Entry,而後再得到相應的指標便可。web

image.png

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;
  }
複製代碼

2. 記錄User Timing

主要是利用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);
      });
    });
  }
複製代碼

3. 記錄First Paint,First Contentful Paint,FirstInputDelay,DataConsumption.

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(獲取瀏覽器名,版本,系統)

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'&emsp;
  | '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));
}
複製代碼

Idle Queue (低優先級任務隊列)

這是谷歌大神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;
  }
複製代碼

Nemetric

很是簡單了,對外暴露參數,而後根據參數調用相應模塊就行。

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來採集用戶的數據。正如 如何採集和分析網頁用戶的性能指標 所說,咱們算用戶指標的平均時長對咱們來講用處不大。利用這些數據咱們能夠

  1. 將性能指標結合地理位置錄入數據庫,造成統計圖。
  2. 大部分用戶的指標的區間以及分佈(哪些國家地區,瀏覽器版本比較慢等等)。
  3. 作相關的A/B test 優化 對比 性能統計區間,提升咱們h5的轉化率。
相關文章
相關標籤/搜索