• DIY一个前端性能采集系统——NVIDIA NeMo Metric实现原理


    Nemo Metric(check the sourceCode)主要分为四个模块:

    performance:主要是performance以及performanceObserver的一些调用的封装。

    detect-browser:用于检测浏览器的名字版本,以及操作系统。

    idle-queue: 实现将任务放入队列,在cpu空闲时候才执行,在这里就是检测到指标数据以后丢到这个队列里面让它来统一处理。

    Nemetric: 供外部调用的类,接受指标参数,采样率,指标检测回调等参数然后调用detect-browser,Idle-queue以及performance实现对性能的采集。

    Performance (核心模块):

    利用PerformancePerformance Timeline API, Navigation Timing API, User Timing API,Resource Timing API获取Navigation指标,资源指标以及用户动作时间指标等,利用PerformanceObserver监听firstPaint,firstContentfulPaint,firstInputDelay等。

    首先是判断方法的支持性,不支持就没办法了。

    static supported(): boolean {
        return (
          window.performance &&
          !!performance.getEntriesByType &&
          !!performance.now &&
          !!performance.mark
        );
     }
    
    static supportedPerformanceObserver(): boolean {
        return (window as any).chrome && 'PerformanceObserver' in window;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    1. 获取Navigation Timing(指标可以继续增加)

    直接 performance.getEntriesByType(‘navigation’)[0] 获取到Navigation 这个Entry,然后再获得相应的指标即可。

    Snipaste_2022-07-30_16-56-51

    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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    2. 记录User Timing

    主要是利用Performance.mark以及Performance.measure方法,核心就是对一个metric记录两遍,然后调用measure 获取得到duration。

     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);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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);
          });
        });
      }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    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);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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();
          });
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    原理都是一样,初始化每个指标,对他们进行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);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    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();
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    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 });
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    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));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126

    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_();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    核心就是 scheduleTasksToRun_,ensureTasksRun_是表示在页面不可见时候任务是否继续进行.

    scheduleTasksToRun_() {
        if (this.ensureTasksRun_ && document.visibilityState === 'hidden') {
          queueMicrotask(this.runTasks_);
        } else {
          if (!this.idleCallbackHandle_) {
            this.idleCallbackHandle_ = rIC(this.runTasks_);
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中 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) => {};
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    而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_));
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    最最核心的就是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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    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();
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    总结

    Nemetric 主要是利用Performace以及Performace Observer来采集用户的数据。正如 如何采集和分析网页用户的性能指标 所说,我们算用户指标的平均时长对我们来说用处不大。利用这些数据我们可以

    1. 将性能指标结合地理位置录入数据库,形成统计图。
    2. 大部分用户的指标的区间以及分布(哪些国家地区,浏览器版本比较慢等等)。
    3. 做相关的A/B test 优化 对比 性能统计区间,提高我们h5的转化率。
  • 相关阅读:
    亚马逊、ozon店铺排名对于卖家来说有多重要?自养号是关键
    企业云成本管控,你真的做对了吗?
    【经历】跨境电商公司目前已在职近2年->丰富且珍贵
    【机器学习】包裹式特征选择之基于遗传算法的特征选择
    面试总结归纳
    产品代码都给你看了,可别再说不会DDD(八):应用服务与领域服务
    MySQL 8.0与MySQL 5.7的binlog差异小结
    Linux socket编程(5):三次握手和四次挥手分析和SIGPIPE信号的处理
    keil stm32f407工程环境搭建
    [附源码]Java计算机毕业设计SSM读书网络社区设计
  • 原文地址:https://blog.csdn.net/qq_53904578/article/details/126074715