前端监控

yuhuo2024-04-11开发知识
参考链接

概述

监控分为异常监控和性能监控,用于主动收集线上异常情况和性能数据。

相关第三方产品:

异常监控

JS 错误

监控代码

window.addEventListener(
    "error",
    (e) => {
        if (e instanceof ErrorEvent) {
            console.log({
                type: "jsError",
                message: e.message, // 错误信息
                filename: e.filename, // 错误发生的文件
                lineno: e.lineno, // 错误发生的行号
                colno: e.colno, // 错误发生的列好
                stack: e.error.stack, // 错误的栈信息
            });
        }
    },
    true
);

触发示例

// SyntaxError 语法错误
{k:};
const a;
let b = 1, b = 2;
             
// ReferenceError 引用错误
notdefined();

// RangeError 范围错误
[].length = -1;

// TypeError 类型错误
new 1;
const a = 1;a = 2;

// URIError URI错误
decodeURI("%");

注意

无法捕获 SyntaxError,因为语法错误出现在解析阶段,还未到运行阶段。

资源加载失败

监控代码

window.addEventListener(
    "error",
    (e) => {
        let url = e.target ? e.target.src || e.target.href : null;
        if (url) {
            console.log({
                type: "resourceError",
                url, // 资源链接
                html: e.target.outerHTML, // 资源元素html
                tagName: e.target.tagName, // 资源元素标签名
            });
        }
    },
    true
);

触发示例

<script src="https://test.cn/xxx.js"></script>
<link href="https://test.cn/xxx.css" rel="stylesheet" />
<img src="https://test.cn/xxx.png" />

Promise 未处理 reject

监控代码

window.addEventListener("unhandledrejection", function (e) {
    console.log({
        type: "promiseError",
        reason: e.reason, // reject原因
    });
});

触发示例

new Promise((resolve, reject) => {
   reject("拒绝");
});

Vue 错误

监控代码

app.config.errorHandler = (error, instance, hook) => {
    console.log({
        subType: "vueError",
        message: error.message, // 错误信息
        stack: error.stack, // 错误的栈信息
        component: instance.$options.name, // 发生错误的组件
        hook,   // 发生错误的钩子
    });
};

触发示例

const app = Vue.createApp({
    name: "root",
    created() {
        [].length = -1;
    },
});
document.addEventListener("DOMContentLoaded", () => {
    app.mount("#app");
});

性能监控

TTFB

Time to First Byte,接收到服务器响应数据第一个字节的时间。TTFB 包含了 DNS、TCP建立、发送请求、服务器处理、接收响应的过程,充分反映网络情况与服务器性能。良好的 TTFB 应该在控制在 800ms 以内。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.log(entry.responseStart);
    }
}).observe({ type: "navigation" });

(entry 类型为 PerformanceNavigationTiming 类

DCL

DOMContentLoaded 事件时间,此时文档解析完成(包含同步加载解析/执行 Css 和 JS),document.readyState = interactive,触发 document 的 DOMContentLoaded 事件。

DCL 之后便是开始渲染页面并加载外部资源,故 DCL 略小于 FP。

// 方式1:监听 navigation 的 domContentLoadedEventEnd 指标
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.log(entry.domContentLoadedEventEnd);
    }
}).observe({ type: "navigation" });

// 方式2:监听 DOMContentLoaded 事件
document.addEventListener("DOMContentLoaded", ()=>{
    console.log(performance.now())
})

(entry 类型为 PerformanceNavigationTiming 类

FP

First Paint,首个任意像素渲染完成的时间,又称白屏时间,即在此时间之前都是白屏。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if(entry.name == "first-paint") {
            console.log(entry.startTime);
        }
    }
}).observe({ type: "paint", buffered: true });

(entry 类型为 PerformancePaintTiming 类

FCP

First Contentful Paint,首个任意内容渲染完成的时间,在此时间之前都视为无内容。内容包含文本、图片、背景图、svg、canvas。

监听 FCP:

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if(entry.name == "first-contentful-paint") {
            console.log(entry.startTime);
        }
    }
}).observe({ type: "paint", buffered: true });

(entry 类型为 PerformancePaintTiming 类

FP 和 FCP 能反映当前页面的网络加载性能情况、DOM 结构复杂度情况、Script 的执行效率情况。良好的 FP / FCP 应该控制在 1.8s 以内。正常情况下测的 FP 和 FCP 的值基本一样,可以通过下面代码验证两者区别。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <script>
            new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    console.log(entry);
                }
            }).observe({ type: "paint", buffered: true });
        </script>
        <style>
            body {
                /* 设置背景色,触发 FP */
                background: gainsboro;
            }
        </style>
    </head>
    <body>
        <!-- 不属于内容,不会触发 FCP -->
        <h1></h1>
        
        <!-- 直接触发 FCP,此时 FP = FCP -->
        <!-- <h1>hello world<h1> -->
        
        <script>
            // 3s后再添加内容,触发 FCP,此时 FCP > FP
            setTimeout(() => {
                let h1 = document.createElement("h1");
                h1.innerText = "你好";
                document.body.append(h1);
            }, 3000);
        </script>
    </body>
</html>

LCP

Largest Contentful Paint,最大内容绘制时间,良好的 LCP 应该控制在 2.5s 以内。LCP 计算的元素范围:

  • <img>元素;
  • 内嵌在 <svg> 元素内的 <image> 元素;
  • <video> 元素(使用封面图像);
  • 通过 url() 函数(而非使用 CSS 渐变)加载的带有背景图像的元素;
  • 包含文本节点或其他行内级文本元素子元素的块级元素;

初次创建 LCP 应该是在页面初始资源加载完成,即 window 的 load 事件触发时(个人推测)。 当后续有更大内容出现时会再次创建一个新的 LCP,即可能存在多个 LCP,直至用户与页面产生第一次交互时停止监听。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    	console.log(entry.startTime);
    }
}).observe({ type: "first-contentful-paint", buffered: true });

(entry 类型为 LargestContentfulPaint 类

L

Load 事件时间,可称为首屏时间,此时文档加载完所有外部资源(图片、视频、iframe 等),document.readyState = complete,触发 window 的 load 事件。

// 方式1:监听 navigation 的 loadEventEnd 指标
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.log(entry.loadEventEnd);
    }
}).observe({ type: "navigation" });

// 方式2:监听 load 事件
window.addEventListener("load", ()=>{
    console.log(performance.now())
})

(entry 类型为 PerformanceNavigationTiming 类

CLS

Cumulative Layout Shift,累计布局偏移,页面元素因动态改变或异步资源加载而发生的位移,是衡量视觉稳定性的核心指标。良好的 CLS 应该控制在 0.1 以内。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    	console.log(entry.value);
    }
}).observe({ type: "layout-shift", buffered: true });

(entry 类型为 LayoutShift 类

FID

First Input Delay,首次交互响应延迟时间,良好的 FID 应该控制在 100ms 以内。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    	console.log(entry.processingStart - entry.startTime);
    }
}).observe({ type: "first-input", buffered: true });

(entry 类型为 PerformanceEventTiming 类

INP

Interaction to Next Paint,交互至下一次绘制时间,良好的 FID 应该控制在 200ms 以内。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    	console.log(entry.duration);
    }
}).observe({ type: "event", buffered: true });

(entry 类型为 PerformanceEventTiming 类

Resource

资源加载信息,包含页面中的所有请求的相关信息。

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.log(entry);
    }
}).observe({ type: "resource", buffered: true });

(entry 类型为 PerformanceResourceTiming 类

Lighthouse

Lighthouse(灯塔)open in new window是谷歌浏览器中一个页面审核工具,提供针对性能、无障碍、最佳做法、SEO 四个方面的审核,生成对应的分数及相关建议。

其中性能方面的分数由 5 个指标构成:

  • First Contentful Paint(FCP):10%
  • Largest Contentful Paint(LCP):25%
  • Cumulative Layout Shift(CLS):25%
  • Speed Index(SI):10%
  • Total Blocking Time(TBT):30%

SI

Speed Index,速度指数,衡量在网页加载期间内容直观地显示的速度,良好的速度指数应该在 3.4s 以内。

TTI

Time to Interactive,页面第一次完全达到可交互状态的时间,需要满足以下条件:

  1. 从 FCP 指标后开始计算;
  2. 持续 5 秒内无长任务(执行时间超过 50ms)且无两个以上正在进行中的 GET 请求;
  3. 往前回溯至 5 秒前的最后一个长任务结束的时间。

TTI 已经从 Lighthouse 的性能指标中移除,并使用更好的 TBT 代替。

比如第一种情况在 10s 内有三个 51ms 的长任务,和第二种情况一个 10s 的长任务,TTI 都是 10s,而这两种情况的性能截然不同。反观 TBT,第一种情况为 3ms,第二种情况为 9950ms,更好的反映页面性能。

TBT

Total Blocking Time, 页面阻塞总时长。计算方法是:将 FCP 和 TTI 之间所有长任务(执行时间超过 50ms)的阻塞部分相加。阻塞部分指 50ms 之后的时间,如时长为 70ms 的长任务,阻塞部分将为 20ms。良好的 TBT 应该在 200ms 以内。

Last Updated 2024/5/15 18:51:03