性能监控--现场统计实现

1,121 阅读3分钟

相关概念请参考: juejin.cn/post/684490… juejin.cn/post/684490…

性能预算

const budget = {
  fcp: 1 * 1000, // 单位:ms
  lcp: 2.5 * 1000, // 单位:ms
  fid: 0.1 * 1000, // 单位:ms
  tbt: 0.3 * 1000, // 单位:ms
  cls: 0.1, // 单位:无
  tti: 5 * 1000, // 单位:ms
  fp: 1 * 1000, // 目前和fcp相同 单位:ms
  fps: 45 // 单位:无, 暂时阈值取值为45};

FPS

直观感受,不同帧率的体验: 50 ~ 60 FPS 相当流畅,30 ~ 50 FPS 舒适度因人而异 30 FPS 以下的动画,让人卡顿和不适感; 帧率波动很大的动画,亦会使人感觉到卡顿。

  var rAF = function () {
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 1000 / 60);
        }
    );
}();
  
var frame = 0;
var allFrameCount = 0;
var lastTime = Date.now();
var lastFameTime = Date.now();
  
var loop = function () {
    var now = Date.now();
    var fs = (now - lastFameTime);
    var fps = Math.round(1000 / fs);
  
    lastFameTime = now;
    // 不置 0,在动画的开头及结尾记录此值的差值算出 FPS
    allFrameCount++;
    frame++;
  
    if (now > 1000 + lastTime) {
        var fps = Math.round((frame * 1000) / (now - lastTime));
        console.log(`${new Date()} 1S内 FPS:`, fps);
        frame = 0;
        lastTime = now;
    };
  
    rAF(loop);
}
 
loop();

FCP

try {
    const po = new PerformanceObserver((list, observer) => {
      for (const entry of list.getEntries()) {
        // 记录条目和所有相关的详细信息。
          if( entry.name === "first-contentful-paint" &&
            entry.entryType === "paint") {
             record({
              type: "fcp",
              value: entry.startTime,
              budget: budget.fcp,
              msg: "首次内容绘制(First Contentful Paint,FCP):"
            });
           }

      }
    });

    po.observe({ type: "paint", buffered: true });
  } catch (e) {
    console.log(e);
  }
}

FP

try {
    const po = new PerformanceObserver((list, observer) => {
      for (const entry of list.getEntries()) {
        // 记录条目和所有相关的详细信息。
          if(entry.name === "first-paint" && entry.entryType === "paint") {
            record({
              type: "fp",
              value: entry.startTime,
              budget: budget.fp,
              msg: "首次绘制即白屏时间(First Paint,FP):"
            });
           }

      }
    });

    po.observe({ type: "paint", buffered: true });
  } catch (e) {
    console.log(e);
  }
}
        

LCP

try {
    const po = new PerformanceObserver((list, observer) => {
      for (const entry of list.getEntries()) {
        // 记录条目和所有相关的详细信息。
          if( entry.entryType === "largest-contentful-paint") {
             record({
              type: "lcp",
              value: entry.startTime,
              budget: budget.lcp,
              msg: "最大内容绘制(Largest Contentful Paint , LCP):"
            });
           }

      }
    });

    po.observe({ type: "largest-contentful-paint", buffered: true });
  } catch (e) {
    console.log(e);
  }
}

FID

try {
    const po = new PerformanceObserver((list, observer) => {
      for (const entry of list.getEntries()) {
        // 记录条目和所有相关的详细信息。
          if(entry.entryType === "first-input") {
             FI(entry, budget);
           }

      }
    });

    po.observe({ type: "first-input", buffered: true });
  } catch (e) {
    console.log(e);
  }
}

function FI(firstInput, budget) {
  // 测量第一次输入延迟(FID)。
  const firstInputDelay = firstInput.processingStart - firstInput.startTime;
  record({
    type: "fid",
    value: firstInputDelay,
    budget: budget.fid,
    msg: "第一次输入延迟(First Input Delay, FID):"
  });
}

CLS

try {
	const takeCls = calcCls();
    const po = new PerformanceObserver((list, observer) => {
      for (const entry of list.getEntries()) {
        // 记录条目和所有相关的详细信息。
          if(entry.entryType === "layout-shift") {
             takeCls(entry, budget);
           }

      }
    });
    
    po.observe({ type: "layout-shift", buffered: true }); // 布局移位
        
  } catch (e) {
    console.log(e);
  }
}


function calcCls() {
  let cls = 0;
  function take(entry, budget) {
    // 计算
    cls += entry.value;
    record({
      type: "cls",
      value: cls,
      budget: budget.cls,
      msg: "累计布局偏移(Cumulative Layout Shift, CLS): "
    });
    return cls;
  }
	return take;
}

TTI 和 TBT

import ttiPolyfill from "tti-polyfill";
try {
	const takeTtiAndTbt = calcByLongtask(budget);
    const po = new PerformanceObserver((list, observer) => {
      for (const entry of list.getEntries()) {
        // 记录条目和所有相关的详细信息。
          if(entry.entryType === "longtask") {  // TTI 和 TBT
             takeTtiAndTbt(entry, budget);
           }

      }
    });
    
    po.observe({ type: "longtask" });
        
  } catch (e) {
    console.log(e);
  }
}

function calcByLongtask(budget) {
let longtasks = [];
let longTaskTimeId = null;
let zeroFctTimeId = null;

 zeroFctTimeId = setTimeout(() => {
  // 当没有长任务执行的时候。
  if (resultData.tti === 0) {
    ttiPolyfill
      .getFirstConsistentlyInteractive({ minValue: resultData.fcp })
      .then(tti => {
        record({
          type: "tti",
          value: tti,
          budget: budget.tti,
          msg: "交互时间3(Time to Interactive , TTI): "
        });
      });

    record({
      type: "tbt",
      value: 0,
      budget: budget.tbt,
      msg: "总阻塞时间3(Total Blocking Time, TBT): "
    });
  }
}, 5 * 1000);


function take(entry, budget) {
  if (resultData.fcp === 0 || resultData.tti !== 0) {
    return;
  }
  clearTimeout(zeroFctTimeId);
  if (resultData.tbt === 0 && resultData.tti === 0) {
    longtasks.push(entry);
  }
  ttiPolyfill
    .getFirstConsistentlyInteractive({ minValue: resultData.fcp })
    .then(tti => {
      record({
        type: "tti",
        value: tti,
        budget: budget.tti,
        msg: "交互时间2(Time to Interactive , TTI): "
      });
      let tbt =
        longtasks
          .map(item => item.duration)
          .reduce((total, next) => total + next) -
        longtasks.length * 50;
      record({
        type: "tbt",
        value: tbt,
        budget: budget.tbt,
        msg: "总阻塞时间2(Total Blocking Time, TBT):  "
      });
    });
}
return take;
}