kibbon / 性能指标自动记录器

// ==UserScript==
// @name         性能指标自动记录器
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  performance api recorder!
// @author       kibbon,GxTongX
// @include      *//*.snssdk.com/feoffline/novel/*
// @match        https://codeday.me/bug/20180703/191609.html
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';
  const storageKeys = {
    active: '__perf_active',
    max: '__perf_max',
    remain: '__perf_remain',
    record: '__perf_record',
  }

  const raf = window.requestAnimationFrame;
  const caf = window.cancelAnimationFrame;
  let firstScreen = 0;
  let list = [];
  const domObserver = new MutationObserver(count);
  domObserver.observe(document, {
    childList: true,
    subtree: true,
  });

  function count() {
    const duration = performance.now();
    const body = document.querySelector('body');
    if (body) {
      list.push({
        score: getScore(body, 1, false),
        time: duration,
      });
    } else {
      list.push({
        score: 0,
        time: duration,
      });
    }
  }

  function getScore(element, depth, exist) {
    let score = 0;
    const tagName = element.tagName;
    if (
      tagName !== 'SCRIPT' &&
      tagName !== 'STYLE' &&
      tagName !== 'META' &&
      tagName !== 'HEAD'
    ) {
      const childrenLength = element.children ? element.children.length : 0;
      if (childrenLength > 0) {
        const children = element.children;
        for (let length = childrenLength - 1; length >= 0; length--) {
          score += getScore(children[length], depth + 1, score > 0);
        }
      }
      // 在以下几种情况下,将当前节点得分设置为0。这个元素没有任何视觉变化的权重
      if (score <= 0 && !exist) {
        // 这个元素没有getBouncingClient方法,比如text?
        if (
          !element.getBoundingClientRect ||
          !typeof element.getBoundingClientRect === 'function'
        ) {
          return 0;
        }

        // 没有获取到boundingClient对象
        const boundingClientRect = element.getBoundingClientRect();
        if (!boundingClientRect) {
          return 0;
        }

        // 当前元素不可见
        if (boundingClientRect.top > window.innerHeight) {
          return 0;
        }

        // 当前元素没有大小
        if (boundingClientRect.height <= 0) {
          return 0;
        }
      }
      // 其他情况下, 为自身加一分,并且按照深度再加0.5倍
      score += 1 + 0.5 * depth;
    }
    return score;
  }


  // 实际计算fmp的操作
  function getFmpInternal() {
    domObserver.disconnect();
    if (!list.length) {
      return 0;
    }

    let target = {
      time: list[0].time,
      rate: 0,
    };

    // 求出最大变动list元素
    for (let s = 1; s < list.length; s++) {
      if (list[s].time >= list[s - 1].time) {
        const diff = list[s].score - list[s - 1].score;
        if (target.rate < diff) {
          target = {
            time: list[s].time,
            rate: diff,
          };
        }
      }
    }
    return target.time;
  }


  function firstScreenMonitor() {
    const getOffsetTop = function (ele) {
      if (!ele) {
        return 0;
      }
      let offsetTop = ele.offsetTop;
      if (ele.offsetParent) {
        offsetTop += getOffsetTop(ele.offsetParent);
      }
      return offsetTop;
    };
    const getWinHeight = function () {
      if (window.innerHeight) {
        return window.innerHeight;
      }
      if (document.documentElement && document.documentElement.clientHeight) {
        return document.documentElement.clientHeight;
      }
    }
    let isFirstScreen = false;

    const firstScreenHeight = getWinHeight() || 0;
    const firstScreenImgs = [];
    let isFindLastImg = false;
    let allImgLoaded = false;
    let timerId;

    function step() {
      let i;
      let img;
      if (isFindLastImg) {
        if (firstScreenImgs.length && !allImgLoaded) {
          for (i = 0; i < firstScreenImgs.length; i++) {
            img = firstScreenImgs[i];
            if (!img.complete) {
              allImgLoaded = false;
              break;
            } else {
              allImgLoaded = true;
            }
          }
        } else {
          allImgLoaded = true;
        }
        if (allImgLoaded) {
          if (!isFirstScreen) {
            isFirstScreen = true;
            firstScreen = performance.now();
            // firstScreen = Date.now() - performance.timing.navigationStart;
          }
          caf(timerId);
          return;
        }
      } else {
        const imgs = document.querySelectorAll('img');
        for (i = 0; i < imgs.length; i++) {
          img = imgs[i];
          const imgOffsetTop = getOffsetTop(img);
          if (imgOffsetTop > firstScreenHeight) {
            isFindLastImg = true;
            break;
          } else if (imgOffsetTop <= firstScreenHeight && !img.hasPushed) {
            img.hasPushed = 1;
            firstScreenImgs.push(img);
          }
        }
      }
      timerId = raf(step);
    }
    timerId = raf(step);
    if (document.readyState === 'complete') {
      allImgLoaded = true;
      isFindLastImg = true;
      caf(timerId);
      step();
      return;
    }
    document.addEventListener('DOMContentLoaded', function() {
      const imgs = document.querySelectorAll('img');
      if (!imgs.length) {
        isFindLastImg = true;
        caf(timerId);
        step();
      }
    });
    window.addEventListener(
      'load',
      function () {
        allImgLoaded = true;
        isFindLastImg = true;
        caf(timerId);
        step();
      },
      false
    );
  }
  firstScreenMonitor();

  let lcp = 0;
  function autoRecorder() {
    const t = window.performance.timing;
    let fid = 0;
    if (performance.getEntriesByType('first-input')[0]) {
      fid = performance.getEntriesByType('first-input')[0].processingStart - performance.getEntriesByType('first-input')[0].startTime;
    }
    const data = {
      blank: t.responseEnd - t.navigationStart, // 白屏
      domready: t.domInteractive - t.navigationStart, // 可交互
      fp: performance.getEntriesByType('paint').filter(function(p) {
          return p.name === 'first-paint';
      })[0].startTime, // FP
      fcp: performance.getEntriesByType('paint').filter(function(p) {
          return p.name === 'first-contentful-paint';
      })[0].startTime, // FCP
      lcp, // LCP
      fmp: getFmpInternal(),
      firstScreen, // 首屏
      load: t.loadEventEnd - t.navigationStart, // 完全加载
      fid // FID
    };
    let remain = parseInt(localStorage.getItem(storageKeys.remain));
    let record = JSON.parse(localStorage.getItem(storageKeys.record));
    record.push([remain, data.blank, data.domready, data.fp, data.fcp, data.fmp, data.lcp, data.firstScreen, data.load, data.fid === 0 ? "-" : data.fid]);
    remain += 1;
    localStorage.setItem(storageKeys.record, JSON.stringify(record));
    const maxium = parseInt(localStorage.getItem(storageKeys.max));
    if (remain > maxium) {
      for (let key in storageKeys) {
        localStorage.removeItem(storageKeys[key]);
      }
      const max = ['最大'];
      const min = ['最小'];
      const avg = ['平均'];
      const len = record[0].length;
      for (let i = 1; i < len; i += 1) {
        let count = 0;
        avg.push(0);
        max.push(0);
        min.push(1e9);
        for (let j = 0; j < maxium; j += 1) {
          if (record[j][i] === "-") {
            continue;
          }
          count++;
          avg[i] += record[j][i];
          min[i] = Math.min(record[j][i], min[i]);
          max[i] = Math.max(record[j][i], max[i]);
        }
        avg[i] /= count;
        if (!count) {
          min[i] = "-";
          max[i] = "-";
          avg[i] = "-";
        }
      }
      record = [avg, max, min].concat(record);
      let recordStr = ',白屏,可交互,FP,FCP,FMP,LCP,首屏,完全加载,FID\n';
      for (let j = 0; j < record.length; j += 1) {
        recordStr += record[j].join(',') + '\n';
      }
      const aLink = document.createElement("a");
      aLink.setAttribute("href", "data:text/csv;charset=utf-8," + encodeURIComponent(recordStr));
      aLink.setAttribute("download", "record_" + location.hostname + location.pathname + ".csv");
      aLink.click();
      alert("记录完成!");
      return;
    }
    localStorage.setItem(storageKeys.remain, remain);
    location.reload();
  }
  const observer = new PerformanceObserver(function(list) {
    for (const entry of list.getEntries()) {
      if (entry.entryType === 'largest-contentful-paint') {
        lcp = entry.renderTime || entry.loadTime;
        checkLoad();
      }
    }
  });
  const checkLoad = function () {
    const t = performance.timing;
    if (t.loadEventEnd > t.navigationStart) {
      autoRecorder();
    } else {
      window.addEventListener('load', function() {
          setTimeout(autoRecorder, 0);
      }, false);
    }
  }
  if (localStorage.getItem(storageKeys.active)) {
    if (lcp) {
      checkLoad();
    } else {
      observer.observe({
        entryTypes: ['paint', 'first-input', 'largest-contentful-paint'],
        buffered: true,
      });
    }
  } else {
    const maxium = Number(window.prompt("刷新次数?", "10"));
    if (maxium) {
      for (let k in storageKeys) {
        localStorage.removeItem(storageKeys[k]);
      }
      localStorage.setItem(storageKeys.active, true);
      localStorage.setItem(storageKeys.remain, 1);
      localStorage.setItem(storageKeys.max, maxium);
      localStorage.setItem(storageKeys.record, JSON.stringify([]));
      location.reload();
    }
  }
})();