<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ SDK - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ SDK - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 05 Jun 2026 20:06:24 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/sdk/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 前端监控 SDK 的技术要点原理分析 ]]>
                </title>
                <description>
                    <![CDATA[ 一个完整的前端监控平台包括三个部分：数据采集与上报、数据整理和存储、数据展示。 本文要讲的就是其中的第一个环节——数据采集与上报。下图是本文要讲述内容的大纲，大家可以先大致了解一下： 仅看理论知识是比较难以理解的，为此我结合本文要讲的技术要点写了一个简单的监控 SDK [https://github.com/woai3c/monitor-demo]，可以用它来写一些简单的 DEMO，帮助加深理解。再结合本文一起阅读，效果更好。 性能数据采集 chrome 开发团队提出了一系列用于检测网页性能的指标：  * FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间  * FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间  * LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间  * CLS(layout-shift)，从页面加载开始和其生命周期状态    [https://developers.google.com/web/upd ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/tech-analysis-of-front-end-monitoring-sdk/</link>
                <guid isPermaLink="false">6165069621a1350622df539c</guid>
                
                    <category>
                        <![CDATA[ SDK ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 13 Oct 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/safar-safarov-koOdUvfGr4c-unsplash-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>一个完整的前端监控平台包括三个部分：数据采集与上报、数据整理和存储、数据展示。</p><p>本文要讲的就是其中的第一个环节——数据采集与上报。下图是本文要讲述内容的大纲，大家可以先大致了解一下：</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97c87a9eb80b4186a462614f206bb7ee~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3633ae631be548baa55fffa97936339e~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>仅看理论知识是比较难以理解的，为此我结合本文要讲的技术要点写了一个简单的<a href="https://github.com/woai3c/monitor-demo">监控 SDK</a>，可以用它来写一些简单的 DEMO，帮助加深理解。再结合本文一起阅读，效果更好。</p><h2 id="-">性能数据采集</h2><p>chrome 开发团队提出了一系列用于检测网页性能的指标：</p><ul><li>FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间</li><li>FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间</li><li>LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间</li><li>CLS(layout-shift)，从页面加载开始和其<a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api">生命周期状态</a>变为隐藏期间发生的所有意外布局偏移的累积分数</li></ul><p>这四个性能指标都需要通过 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver">PerformanceObserver</a> 来获取（也可以通过 <code>performance.getEntriesByName()</code> 获取，但它不是在事件触发时通知的）。PerformanceObserver 是一个性能监测对象，用于监测性能度量事件。</p><h3 id="fp">FP</h3><p>FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间。其实把 FP 理解成白屏时间也是没问题的。</p><p>测量代码如下：</p><pre><code class="language-js">const entryHandler = (list) =&gt; {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-paint') {
            observer.disconnect()
        }

       console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
// buffered 属性表示是否观察缓存数据，也就是说观察代码添加时机比事情触发时机晚也没关系。
observer.observe({ type: 'paint', buffered: true })</code></pre><p>通过以上代码可以得到 FP 的内容:</p><pre><code class="language-js">{
    duration:&nbsp;0,
    entryType:&nbsp;"paint",
    name:&nbsp;"first-paint",
    startTime:&nbsp;359, // fp 时间
}</code></pre><p>其中 <code>startTime</code> 就是我们要的绘制时间。</p><h3 id="fcp">FCP</h3><p>FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标，"内容"指的是文本、图像（包括背景图像）、<code>&lt;svg&gt;</code>元素或非白色的<code>&lt;canvas&gt;</code>元素。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4f1c9b61029448dae2b1cfb57b4ef75~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>为了提供良好的用户体验，FCP 的分数应该控制在 1.8 秒以内。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9818c66879b345e3b4845ff3fe01e8c9~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>测量代码：</p><pre><code class="language-js">const entryHandler = (list) =&gt; {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
            observer.disconnect()
        }
        
        console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'paint', buffered: true })</code></pre><p>通过以上代码可以得到 FCP 的内容:</p><pre><code class="language-js">{
    duration:&nbsp;0,
    entryType:&nbsp;"paint",
    name:&nbsp;"first-contentful-paint",
    startTime:&nbsp;459, // fcp 时间
}</code></pre><p>其中 <code>startTime</code> 就是我们要的绘制时间。</p><h3 id="lcp">LCP</h3><p>LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间。LCP 指标会根据页面<a href="https://web.dev/lcp/#what-elements-are-considered">首次开始加载</a>的时间点来报告可视区域内可见的最大<a href="https://web.dev/lcp/#what-elements-are-considered">图像或文本块</a>完成渲染的相对时间。</p><p>一个良好的 LCP 分数应该控制在 2.5 秒以内。</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c090dd8b042c46d2adaba5395ca68f47~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>测量代码：</p><pre><code class="language-js">const entryHandler = (list) =&gt; {
    if (observer) {
        observer.disconnect()
    }

    for (const entry of list.getEntries()) {
        console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'largest-contentful-paint', buffered: true })</code></pre><p>通过以上代码可以得到 LCP 的内容:</p><pre><code class="language-js">{
    duration: 0,
    element: p,
    entryType: "largest-contentful-paint",
    id: "",
    loadTime: 0,
    name: "",
    renderTime: 1021.299,
    size: 37932,
    startTime: 1021.299,
    url: "",
}</code></pre><p>其中 <code>startTime</code> 就是我们要的绘制时间。element 是指 LCP 绘制的 DOM 元素。</p><p>FCP 和 LCP 的区别是：FCP 只要任意内容绘制完成就触发，LCP 是最大内容渲染完成时触发。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e64637ac9d243a58101d8ed01fe886e~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>LCP 考察的元素类型为：</p><ul><li><code>&lt;img&gt;</code>元素</li><li>内嵌在<code>&lt;svg&gt;</code>元素内的<code>&lt;image&gt;</code>元素</li><li><code>&lt;video&gt;</code>元素（使用封面图像）</li><li>通过<a href="https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fdocs%2FWeb%2FCSS%2Furl()" rel="nofollow noopener noreferrer"><code>url()</code></a>函数（而非使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Images/Using_CSS_gradients">CSS 渐变</a>）加载的带有背景图像的元素</li><li>包含文本节点或其他行内级文本元素子元素的<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Block-level_elements">块级元素</a>。</li></ul><h3 id="cls">CLS</h3><p>CLS(layout-shift)，从页面加载开始和其<a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api">生命周期状态</a>变为隐藏期间发生的所有意外布局偏移的累积分数。</p><p>布局偏移分数的计算方式如下：</p><pre><code>布局偏移分数 = 影响分数 * 距离分数</code></pre><p><a href="https://github.com/WICG/layout-instability#Impact-Fraction">影响分数</a>测量<em>不稳定元素</em>对两帧之间的可视区域产生的影响。</p><p><em>距离分数</em>指的是任何<em>不稳定元素</em>在一帧中位移的最大距离（水平或垂直）除以可视区域的最大尺寸维度（宽度或高度，以较大者为准）。</p><p><strong>CLS 就是把所有布局偏移分数加起来的总和</strong>。</p><p>当一个 DOM 在两个渲染帧之间产生了位移，就会触发 CLS（如图所示）。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ff07d41c624248a1b66c5761f0482f2c~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0d5ab8100c9489a991dd0be8e198af0~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>上图中的矩形从左上角移动到了右边，这就算是一次布局偏移。同时，在 CLS 中，有一个叫<strong>会话窗口</strong>的术语：一个或多个快速连续发生的单次布局偏移，每次偏移相隔的时间少于 1 秒，且整个窗口的最大持续时长为 5 秒。</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6af2ec569644013962645820efb16d3~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>例如上图中的第二个会话窗口，它里面有四次布局偏移，每一次偏移之间的间隔必须少于 1 秒，并且第一个偏移和最后一个偏移之间的时间不能超过 5 秒，这样才能算是一次会话窗口。如果不符合这个条件，就算是一个新的会话窗口。可能有人会问，为什么要这样规定？其实这是 chrome 团队根据大量的实验和研究得出的分析结果 <a href="https://web.dev/evolving-cls/">Evolving the CLS metric</a>。</p><p>CLS 一共有三种计算方式：</p><ol><li>累加</li><li>取所有会话窗口的平均数</li><li>取所有会话窗口中的最大值</li></ol><h4 id="--1">累加</h4><p>也就是把从页面加载开始的所有布局偏移分数加在一起。但是这种计算方式对生命周期长的页面不友好，页面存留时间越长，CLS 分数越高。</p><h4 id="--2">取所有会话窗口的平均数</h4><p>这种计算方式不是按单个布局偏移为单位，而是以会话窗口为单位。将所有会话窗口的值相加再取平均值。但是这种计算方式也有缺点。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/42e5208d83f349db84cf4a27194a57f2~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>从上图可以看出来，第一个会话窗口产生了比较大的 CLS 分数，第二个会话窗口产生了比较小的 CLS 分数。如果取它们的平均值来当做 CLS 分数，则根本看不出来页面的运行状况。原来页面是早期偏移多，后期偏移少，现在的平均值无法反映出这种情况。</p><h4 id="--3">取所有会话窗口中的最大值</h4><p>这种方式是目前最优的计算方式，每次只取所有会话窗口的最大值，用来反映页面布局偏移的最差情况。详情请看 <a href="https://web.dev/evolving-cls/">Evolving the CLS metric</a>。</p><p>下面是第三种计算方式的测量代码：</p><pre><code class="language-js">let sessionValue = 0
let sessionEntries = []
const cls = {
    subType: 'layout-shift',
    name: 'layout-shift',
    type: 'performance',
    pageURL: getPageURL(),
    value: 0,
}

const entryHandler = (list) =&gt; {
    for (const entry of list.getEntries()) {
        // Only count layout shifts without recent user input.
        if (!entry.hadRecentInput) {
            const firstSessionEntry = sessionEntries[0]
            const lastSessionEntry = sessionEntries[sessionEntries.length - 1]

            // If the entry occurred less than 1 second after the previous entry and
            // less than 5 seconds after the first entry in the session, include the
            // entry in the current session. Otherwise, start a new session.
            if (
                sessionValue
                &amp;&amp; entry.startTime - lastSessionEntry.startTime &lt; 1000
                &amp;&amp; entry.startTime - firstSessionEntry.startTime &lt; 5000
            ) {
                sessionValue += entry.value
                sessionEntries.push(formatCLSEntry(entry))
            } else {
                sessionValue = entry.value
                sessionEntries = [formatCLSEntry(entry)]
            }

            // If the current session value is larger than the current CLS value,
            // update CLS and the entries contributing to it.
            if (sessionValue &gt; cls.value) {
                cls.value = sessionValue
                cls.entries = sessionEntries
                cls.startTime = performance.now()
                lazyReportCache(deepCopy(cls))
            }
        }
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'layout-shift', buffered: true })</code></pre><p>在看完上面的文字描述后，再看代码就好理解了。一次布局偏移的测量内容如下：</p><pre><code class="language-js">{
  duration: 0,
  entryType: "layout-shift",
  hadRecentInput: false,
  lastInputTime: 0,
  name: "",
  sources: (2) [LayoutShiftAttribution, LayoutShiftAttribution],
  startTime: 1176.199999999255,
  value: 0.000005752046026677329,
}</code></pre><p>代码中的 <code>value</code> 字段就是布局偏移分数。</p><h3 id="domcontentloaded-load-">DOMContentLoaded、load 事件</h3><p>当纯 HTML 被完全加载以及解析时，<code>DOMContentLoaded</code> 事件会被触发，不用等待 css、img、iframe 加载完。</p><p>当整个页面及所有依赖资源如样式表和图片都已完成加载时，将触发 <code>load</code> 事件。</p><p>虽然这两个性能指标比较旧了，但是它们仍然能反映页面的一些情况。对于它们进行监听仍然是必要的。</p><pre><code class="language-js">import { lazyReportCache } from '../utils/report'

['load', 'DOMContentLoaded'].forEach(type =&gt; onEvent(type))

function onEvent(type) {
    function callback() {
        lazyReportCache({
            type: 'performance',
            subType: type.toLocaleLowerCase(),
            startTime: performance.now(),
        })

        window.removeEventListener(type, callback, true)
    }

    window.addEventListener(type, callback, true)
}</code></pre><h3 id="--4">首屏渲染时间</h3><p>大多数情况下，首屏渲染时间可以通过 <code>load</code> 事件获取。除了一些特殊情况，例如异步加载的图片和 DOM。</p><pre><code class="language-html">&lt;script&gt;
    setTimeout(() =&gt; {
        document.body.innerHTML = `
            &lt;div&gt;
                &lt;!-- 省略一堆代码... --&gt;
            &lt;/div&gt;
        `
    }, 3000)
&lt;/script&gt;</code></pre><p>像这种情况就无法通过 <code>load</code> 事件获取首屏渲染时间了。这时我们需要通过 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver/MutationObserver">MutationObserver</a> 来获取首屏渲染时间。MutationObserver 在监听的 DOM 元素属性发生变化时会触发事件。</p><p>首屏渲染时间计算过程：</p><ol><li>利用 MutationObserver 监听 document 对象，每当 DOM 元素属性发生变更时，触发事件。</li><li>判断该 DOM 元素是否在首屏内，如果在，则在 <code>requestAnimationFrame()</code> 回调函数中调用 <code>performance.now()</code> 获取当前时间，作为它的绘制时间。</li><li>将最后一个 DOM 元素的绘制时间和首屏中所有加载的图片时间作对比，将最大值作为首屏渲染时间。</li></ol><h4 id="-dom">监听 DOM</h4><pre><code class="language-js">const next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout
const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK']
    
observer = new MutationObserver(mutationList =&gt; {
    const entry = {
        children: [],
    }

    for (const mutation of mutationList) {
        if (mutation.addedNodes.length &amp;&amp; isInScreen(mutation.target)) {
             // ...
        }
    }

    if (entry.children.length) {
        entries.push(entry)
        next(() =&gt; {
            entry.startTime = performance.now()
        })
    }
})

observer.observe(document, {
    childList: true,
    subtree: true,
})</code></pre><p>上面的代码就是监听 DOM 变化的代码，同时需要过滤掉 <code>style</code>、<code>script</code>、<code>link</code> 等标签。</p><h4 id="--5">判断是否在首屏</h4><p>一个页面的内容可能非常多，但用户最多只能看见一屏幕的内容。所以在统计首屏渲染时间的时候，需要限定范围，把渲染内容限定在当前屏幕内。</p><pre><code class="language-js">const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight

// dom 对象是否在屏幕内
function isInScreen(dom) {
    const rectInfo = dom.getBoundingClientRect()
    if (rectInfo.left &lt; viewportWidth &amp;&amp; rectInfo.top &lt; viewportHeight) {
        return true
    }

    return false
}</code></pre><h4 id="-requestanimationframe-dom-">使用 <code>requestAnimationFrame()</code> 获取 DOM 绘制时间</h4><p>当 DOM 变更触发 MutationObserver 事件时，只是代表 DOM 内容可以被读取到，并不代表该 DOM 被绘制到了屏幕上。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/67230c5e58ff4c699be7758656e4504f~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>从上图可以看出，当触发 MutationObserver 事件时，可以读取到 <code>document.body</code> 上已经有内容了，但实际上左边的屏幕并没有绘制任何内容。所以要调用 <code>requestAnimationFrame()</code> 在浏览器绘制成功后再获取当前时间作为 DOM 绘制时间。</p><h4 id="--6">和首屏内的所有图片加载时间作对比</h4><pre><code class="language-js">function getRenderTime() {
    let startTime = 0
    entries.forEach(entry =&gt; {
        if (entry.startTime &gt; startTime) {
            startTime = entry.startTime
        }
    })

    // 需要和当前页面所有加载图片的时间做对比，取最大值
    // 图片请求时间要小于 startTime，响应结束时间要大于 startTime
    performance.getEntriesByType('resource').forEach(item =&gt; {
        if (
            item.initiatorType === 'img'
            &amp;&amp; item.fetchStart &lt; startTime 
            &amp;&amp; item.responseEnd &gt; startTime
        ) {
            startTime = item.responseEnd
        }
    })
    
    return startTime
}</code></pre><h4 id="--7">优化</h4><p>现在的代码还没优化完，主要有两点注意事项：</p><ol><li>什么时候上报渲染时间？</li><li>如果兼容异步添加 DOM 的情况？</li></ol><p>第一点，必须要在 DOM 不再变化后再上报渲染时间，一般 load 事件触发后，DOM 就不再变化了。所以我们可以在这个时间点进行上报。</p><p>第二点，可以在 LCP 事件触发后再进行上报。不管是同步还是异步加载的 DOM，它都需要进行绘制，所以可以监听 LCP 事件，在该事件触发后才允许进行上报。</p><p>将以上两点方案结合在一起，就有了以下代码：</p><pre><code class="language-js">let isOnLoaded = false
executeAfterLoad(() =&gt; {
    isOnLoaded = true
})


let timer
let observer
function checkDOMChange() {
    clearTimeout(timer)
    timer = setTimeout(() =&gt; {
        // 等 load、lcp 事件触发后并且 DOM 树不再变化时，计算首屏渲染时间
        if (isOnLoaded &amp;&amp; isLCPDone()) {
            observer &amp;&amp; observer.disconnect()
            lazyReportCache({
                type: 'performance',
                subType: 'first-screen-paint',
                startTime: getRenderTime(),
                pageURL: getPageURL(),
            })

            entries = null
        } else {
            checkDOMChange()
        }
    }, 500)
}</code></pre><p><code>checkDOMChange()</code> 代码每次在触发 MutationObserver 事件时进行调用，需要用防抖函数进行处理。</p><h3 id="--8">接口请求耗时</h3><p>接口请求耗时需要对 XMLHttpRequest 和 fetch 进行监听。</p><p><strong>监听 XMLHttpRequest</strong></p><pre><code class="language-js">originalProto.open = function newOpen(...args) {
    this.url = args[1]
    this.method = args[0]
    originalOpen.apply(this, args)
}

originalProto.send = function newSend(...args) {
    this.startTime = Date.now()

    const onLoadend = () =&gt; {
        this.endTime = Date.now()
        this.duration = this.endTime - this.startTime

        const { status, duration, startTime, endTime, url, method } = this
        const reportData = {
            status,
            duration,
            startTime,
            endTime,
            url,
            method: (method || 'GET').toUpperCase(),
            success: status &gt;= 200 &amp;&amp; status &lt; 300,
            subType: 'xhr',
            type: 'performance',
        }

        lazyReportCache(reportData)

        this.removeEventListener('loadend', onLoadend, true)
    }

    this.addEventListener('loadend', onLoadend, true)
    originalSend.apply(this, args)
}</code></pre><p>如何判断 XML 请求是否成功？可以根据他的状态码是否在 200~299 之间。如果在，那就是成功，否则失败。</p><p><strong>监听 fetch</strong></p><pre><code class="language-js">const originalFetch = window.fetch

function overwriteFetch() {
    window.fetch = function newFetch(url, config) {
        const startTime = Date.now()
        const reportData = {
            startTime,
            url,
            method: (config?.method || 'GET').toUpperCase(),
            subType: 'fetch',
            type: 'performance',
        }

        return originalFetch(url, config)
        .then(res =&gt; {
            reportData.endTime = Date.now()
            reportData.duration = reportData.endTime - reportData.startTime

            const data = res.clone()
            reportData.status = data.status
            reportData.success = data.ok

            lazyReportCache(reportData)

            return res
        })
        .catch(err =&gt; {
            reportData.endTime = Date.now()
            reportData.duration = reportData.endTime - reportData.startTime
            reportData.status = 0
            reportData.success = false

            lazyReportCache(reportData)

            throw err
        })
    }
}</code></pre><p>对于 fetch，可以根据返回数据中的的 <code>ok</code> 字段判断请求是否成功，如果为 <code>true</code> 则请求成功，否则失败。</p><p><strong>注意</strong>，监听到的接口请求时间和 chrome devtool 上检测到的时间可能不一样。这是因为 chrome devtool 上检测到的是 HTTP 请求发送和接口整个过程的时间。但是 xhr 和 fetch 是异步请求，接口请求成功后需要调用回调函数。事件触发时会把回调函数放到消息队列，然后浏览器再处理，这中间也有一个等待过程。</p><h3 id="--9">资源加载时间、缓存命中率</h3><p>通过 <code>PerformanceObserver</code> 可以监听 <code>resource</code> 和 <code>navigation</code> 事件，如果浏览器不支持 <code>PerformanceObserver</code>，还可以通过 <code>performance.getEntriesByType(entryType)</code> 来进行降级处理。</p><p>当 <code>resource</code> 事件触发时，可以获取到对应的资源列表，每个资源对象包含以下一些字段：</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e6cb30ae9a4447bbe43bfcff6c6c4a1~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>从这些字段中我们可以提取到一些有用的信息：</p><pre><code class="language-js">{
    name: entry.name, // 资源名称
    subType: entryType,
    type: 'performance',
    sourceType: entry.initiatorType, // 资源类型
    duration: entry.duration, // 资源加载耗时
    dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS 耗时
    tcp: entry.connectEnd - entry.connectStart, // 建立 tcp 连接耗时
    redirect: entry.redirectEnd - entry.redirectStart, // 重定向耗时
    ttfb: entry.responseStart, // 首字节时间
    protocol: entry.nextHopProtocol, // 请求协议
    responseBodySize: entry.encodedBodySize, // 响应内容大小
    responseHeaderSize: entry.transferSize - entry.encodedBodySize, // 响应头部大小
    resourceSize: entry.decodedBodySize, // 资源解压后的大小
    isCache: isCache(entry), // 是否命中缓存
    startTime: performance.now(),
}</code></pre><p><strong>判断该资源是否命中缓存</strong></p><p>在这些资源对象中有一个 &nbsp;<code>transferSize</code> 字段，它表示获取资源的大小，包括响应头字段和响应数据的大小。如果这个值为 0，说明是从缓存中直接读取的（强制缓存）。如果这个值不为 0，但是 <code>encodedBodySize</code> 字段为 0，说明它走的是协商缓存（<code>encodedBodySize</code> 表示请求响应数据 body 的大小）。</p><pre><code class="language-js">function isCache(entry) {
    // 直接从缓存读取或 304
    return entry.transferSize === 0 || (entry.transferSize !== 0 &amp;&amp; entry.encodedBodySize === 0)
}</code></pre><p>不符合以上条件的，说明未命中缓存。然后将<code>所有命中缓存的数据/总数据</code>就能得出缓存命中率。</p><h3 id="-bfc-back-forward-cache-">浏览器往返缓存 BFC（back/forward cache）</h3><p>bfcache 是一种内存缓存，它会将整个页面保存在内存中。当用户返回时可以马上看到整个页面，而不用再次刷新。据该文章 <a href="https://web.dev/bfcache/">bfcache</a> 介绍，firfox 和 safari 一直支持 bfc，chrome 只有在高版本的移动端浏览器支持。但我试了一下，只有 safari 浏览器支持，可能我的 firfox 版本不对。</p><p>但是 bfc 也是有缺点的，当用户返回并从 bfc 中恢复页面时，原来页面的代码不会再次执行。为此，浏览器提供了一个 <code>pageshow</code> 事件，可以把需要再次执行的代码放在里面。</p><pre><code class="language-js">window.addEventListener('pageshow', function(event) {
  // 如果该属性为 true，表示是从 bfc 中恢复的页面
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});</code></pre><p>从 bfc 中恢复的页面，我们也需要收集他们的 FP、FCP、LCP 等各种时间。</p><pre><code class="language-js">onBFCacheRestore(event =&gt; {
    requestAnimationFrame(() =&gt; {
        ['first-paint', 'first-contentful-paint'].forEach(type =&gt; {
            lazyReportCache({
                startTime: performance.now() - event.timeStamp,
                name: type,
                subType: type,
                type: 'performance',
                pageURL: getPageURL(),
                bfc: true,
            })
        })
    })
})</code></pre><p>上面的代码很好理解，在 <code>pageshow</code> 事件触发后，用当前时间减去事件触发时间，这个时间差值就是性能指标的绘制时间。<strong>注意</strong>，从 bfc 中恢复的页面的这些性能指标，值一般都很小，一般在 10 ms 左右。所以要给它们加个标识字段 <code>bfc: true</code>。这样在做性能统计时可以对它们进行忽略。</p><h3 id="fps">FPS</h3><p>利用 <code>requestAnimationFrame()</code> 我们可以计算当前页面的 FPS。</p><pre><code class="language-js">const next = window.requestAnimationFrame 
    ? requestAnimationFrame : (callback) =&gt; { setTimeout(callback, 1000 / 60) }

const frames = []

export default function fps() {
    let frame = 0
    let lastSecond = Date.now()

    function calculateFPS() {
        frame++
        const now = Date.now()
        if (lastSecond + 1000 &lt;= now) {
            // 由于 now - lastSecond 的单位是毫秒，所以 frame 要 * 1000
            const fps = Math.round((frame * 1000) / (now - lastSecond))
            frames.push(fps)
                
            frame = 0
            lastSecond = now
        }
    
        // 避免上报太快，缓存一定数量再上报
        if (frames.length &gt;= 60) {
            report(deepCopy({
                frames,
                type: 'performace',
                subType: 'fps',
            }))
    
            frames.length = 0
        }

        next(calculateFPS)
    }

    calculateFPS()
}</code></pre><p>代码逻辑如下：</p><ol><li>先记录一个初始时间，然后每次触发 <code>requestAnimationFrame()</code> 时，就将帧数加 1。过去一秒后用<code>帧数/流逝的时间</code>就能得到当前帧率。</li></ol><p>当连续三个低于 20 的 FPS 出现时，我们可以断定页面出现了卡顿，详情请看 <a href="https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F39292837" rel="nofollow noopener noreferrer">如何监控网页的卡顿</a>。</p><pre><code class="language-js">export function isBlocking(fpsList, below = 20, last = 3) {
    let count = 0
    for (let i = 0; i &lt; fpsList.length; i++) {
        if (fpsList[i] &amp;&amp; fpsList[i] &lt; below) {
            count++
        } else {
            count = 0
        }

        if (count &gt;= last) {
            return true
        }
    }

    return false
}</code></pre><h3 id="vue-">Vue 路由变更渲染时间</h3><p>首屏渲染时间我们已经知道如何计算了，但是如何计算 SPA 应用的页面路由切换导致的页面渲染时间呢？本文用 Vue 作为示例，讲一下我的思路。</p><pre><code class="language-js">export default function onVueRouter(Vue, router) {
    let isFirst = true
    let startTime
    router.beforeEach((to, from, next) =&gt; {
        // 首次进入页面已经有其他统计的渲染时间可用
        if (isFirst) {
            isFirst = false
            return next()
        }

        // 给 router 新增一个字段，表示是否要计算渲染时间
        // 只有路由跳转才需要计算
        router.needCalculateRenderTime = true
        startTime = performance.now()

        next()
    })

    let timer
    Vue.mixin({
        mounted() {
            if (!router.needCalculateRenderTime) return

            this.$nextTick(() =&gt; {
                // 仅在整个视图都被渲染之后才会运行的代码
                const now = performance.now()
                clearTimeout(timer)

                timer = setTimeout(() =&gt; {
                    router.needCalculateRenderTime = false
                    lazyReportCache({
                        type: 'performance',
                        subType: 'vue-router-change-paint',
                        duration: now - startTime,
                        startTime: now,
                        pageURL: getPageURL(),
                    })
                }, 1000)
            })
        },
    })
}</code></pre><p>代码逻辑如下：</p><ol><li>监听路由钩子，在路由切换时会触发 <code>router.beforeEach()</code> 钩子，在该钩子的回调函数里将当前时间记为渲染开始时间。</li><li>利用 <code>Vue.mixin()</code> 对所有组件的 <code>mounted()</code> 注入一个函数。每个函数都执行一个防抖函数。</li><li>当最后一个组件的 <code>mounted()</code> 触发时，就代表该路由下的所有组件已经挂载完毕。可以在 <code>this.$nextTick()</code> 回调函数中获取渲染时间。</li></ol><p>同时，还要考虑到一个情况。不切换路由时，也会有变更组件的情况，这时不应该在这些组件的 <code>mounted()</code> 里进行渲染时间计算。所以需要添加一个 <code>needCalculateRenderTime</code> 字段，当切换路由时将它设为 true，代表可以计算渲染时间了。</p><h2 id="--10">错误数据采集</h2><h3 id="--11">资源加载错误</h3><p>使用 <code>addEventListener()</code> 监听 error 事件，可以捕获到资源加载失败错误。</p><pre><code class="language-js">// 捕获资源加载失败错误 js css img...
window.addEventListener('error', e =&gt; {
    const target = e.target
    if (!target) return

    if (target.src || target.href) {
        const url = target.src || target.href
        lazyReportCache({
            url,
            type: 'error',
            subType: 'resource',
            startTime: e.timeStamp,
            html: target.outerHTML,
            resourceType: target.tagName,
            paths: e.path.map(item =&gt; item.tagName).filter(Boolean),
            pageURL: getPageURL(),
        })
    }
}, true)</code></pre><h3 id="js-">js 错误</h3><p>使用 <code>window.onerror</code> 可以监听 js 错误。</p><pre><code class="language-js">// 监听 js 错误
window.onerror = (msg, url, line, column, error) =&gt; {
    lazyReportCache({
        msg,
        line,
        column,
        error: error.stack,
        subType: 'js',
        pageURL: url,
        type: 'error',
        startTime: performance.now(),
    })
}</code></pre><h3 id="promise-">promise 错误</h3><p>使用 <code>addEventListener()</code> 监听 unhandledrejection 事件，可以捕获到未处理的 promise 错误。</p><pre><code class="language-js">// 监听 promise 错误 缺点是获取不到列数据
window.addEventListener('unhandledrejection', e =&gt; {
    lazyReportCache({
        reason: e.reason?.stack,
        subType: 'promise',
        type: 'error',
        startTime: e.timeStamp,
        pageURL: getPageURL(),
    })
})</code></pre><h3 id="sourcemap">sourcemap</h3><p>一般生产环境的代码都是经过压缩的，并且生产环境不会把 sourcemap 文件上传。所以生产环境上的代码报错信息是很难读的。因此，我们可以利用 <a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fmozilla%2Fsource-map" rel="nofollow noopener noreferrer">source-map</a> 来对这些压缩过的代码报错信息进行还原。</p><p>当代码报错时，我们可以获取到对应的文件名、行数、列数:</p><pre><code class="language-js">{
    line: 1,
    column: 17,
    file: 'https:/www.xxx.com/bundlejs',
}</code></pre><p>然后调用下面的代码进行还原：</p><pre><code class="language-js">async function parse(error) {
    const mapObj = JSON.parse(getMapFileContent(error.url))
    const consumer = await new sourceMap.SourceMapConsumer(mapObj)
    // 将 webpack://source-map-demo/./src/index.js 文件中的 ./ 去掉
    const sources = mapObj.sources.map(item =&gt; format(item))
    // 根据压缩后的报错信息得出未压缩前的报错行列数和源码文件
    const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
    // sourcesContent 中包含了各个文件的未压缩前的源码，根据文件名找出对应的源码
    const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
    return {
        file: originalInfo.source,
        content: originalFileContent,
        line: originalInfo.line,
        column: originalInfo.column,
        msg: error.msg,
        error: error.error
    }
}

function format(item) {
    return item.replace(/(\.\/)*/g, '')
}

function getMapFileContent(url) {
    return fs.readFileSync(path.resolve(__dirname, `./maps/${url.split('/').pop()}.map`), 'utf-8')
}</code></pre><p>每次项目打包时，如果开启了 sourcemap，那么每一个 js 文件都会有一个对应的 map 文件。</p><pre><code>bundle.js
bundle.js.map</code></pre><p>这时 js 文件放在静态服务器上供用户访问，map 文件存储在服务器，用于还原错误信息。<code>source-map</code> 库可以根据压缩过的代码报错信息还原出未压缩前的代码报错信息。例如压缩后报错位置为 <code>1 行 47 列</code>，还原后真正的位置可能为 <code>4 行 10 列</code>。除了位置信息，还可以获取到源码原文。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b1c6b5eebb7b4ef59d4dd6ad613484eb~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>上图就是一个代码报错还原后的示例。鉴于这部分内容不属于 SDK 的范围，所以我另开了一个 <a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fwoai3c%2Fsource-map-demo" rel="nofollow noopener noreferrer">仓库</a> 来做这个事，有兴趣可以看看。</p><h3 id="vue--1">Vue 错误</h3><p>利用 <code>window.onerror</code> 是捕获不到 Vue 错误的，它需要使用 Vue 提供的 API 进行监听。</p><pre><code class="language-js">Vue.config.errorHandler = (err, vm, info) =&gt; {
    // 将报错信息打印到控制台
    console.error(err)

    lazyReportCache({
        info,
        error: err.stack,
        subType: 'vue',
        type: 'error',
        startTime: performance.now(),
        pageURL: getPageURL(),
    })
}</code></pre><h2 id="--12">行为数据采集</h2><h3 id="pv-uv">PV、UV</h3><p>PV(page view) 是页面浏览量，UV(Unique visitor)用户访问量。PV 只要访问一次页面就算一次，UV 同一天内多次访问只算一次。</p><p>对于前端来说，只要每次进入页面上报一次 PV 就行，UV 的统计放在服务端来做，主要是分析上报的数据来统计得出 UV。</p><pre><code class="language-js">export default function pv() {
    lazyReportCache({
        type: 'behavior',
        subType: 'pv',
        startTime: performance.now(),
        pageURL: getPageURL(),
        referrer: document.referrer,
        uuid: getUUID(),
    })
}</code></pre><h3 id="--13">页面停留时长</h3><p>用户进入页面记录一个初始时间，用户离开页面时用当前时间减去初始时间，就是用户停留时长。这个计算逻辑可以放在 <code>beforeunload</code> 事件里做。</p><pre><code class="language-js">export default function pageAccessDuration() {
    onBeforeunload(() =&gt; {
        report({
            type: 'behavior',
            subType: 'page-access-duration',
            startTime: performance.now(),
            pageURL: getPageURL(),
            uuid: getUUID(),
        }, true)
    })
}</code></pre><h3 id="--14">页面访问深度</h3><p>记录页面访问深度是很有用的，例如不同的活动页面 a 和 b。a 平均访问深度只有 50%，b 平均访问深度有 80%，说明 b 更受用户喜欢，根据这一点可以有针对性的修改 a 活动页面。</p><p>除此之外还可以利用访问深度以及停留时长来鉴别电商刷单。例如有人进来页面后一下就把页面拉到底部然后等待一段时间后购买，有人是慢慢的往下滚动页面，最后再购买。虽然他们在页面的停留时间一样，但明显第一个人更像是刷单的。</p><p>页面访问深度计算过程稍微复杂一点：</p><ol><li>用户进入页面时，记录当前时间、scrollTop 值、页面可视高度、页面总高度。</li><li>用户滚动页面的那一刻，会触发 <code>scroll</code> 事件，在回调函数中用第一点得到的数据算出页面访问深度和停留时长。</li><li>当用户滚动页面到某一点时，停下继续观看页面。这时记录当前时间、scrollTop 值、页面可视高度、页面总高度。</li><li>重复第二点...</li></ol><p>具体代码请看：</p><pre><code class="language-js">let timer
let startTime = 0
let hasReport = false
let pageHeight = 0
let scrollTop = 0
let viewportHeight = 0

export default function pageAccessHeight() {
    window.addEventListener('scroll', onScroll)

    onBeforeunload(() =&gt; {
        const now = performance.now()
        report({
            startTime: now,
            duration: now - startTime,
            type: 'behavior',
            subType: 'page-access-height',
            pageURL: getPageURL(),
            value: toPercent((scrollTop + viewportHeight) / pageHeight),
            uuid: getUUID(),
        }, true)
    })

    // 页面加载完成后初始化记录当前访问高度、时间
    executeAfterLoad(() =&gt; {
        startTime = performance.now()
        pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop
        viewportHeight = window.innerHeight
    })
}

function onScroll() {
    clearTimeout(timer)
    const now = performance.now()
    
    if (!hasReport) {
        hasReport = true
        lazyReportCache({
            startTime: now,
            duration: now - startTime,
            type: 'behavior',
            subType: 'page-access-height',
            pageURL: getPageURL(),
            value: toPercent((scrollTop + viewportHeight) / pageHeight),
            uuid: getUUID(),
        })
    }

    timer = setTimeout(() =&gt; {
        hasReport = false
        startTime = now
        pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop
        viewportHeight = window.innerHeight        
    }, 500)
}

function toPercent(val) {
    if (val &gt;= 1) return '100%'
    return (val * 100).toFixed(2) + '%'
}</code></pre><h3 id="--15">用户点击</h3><p>利用 <code>addEventListener()</code> 监听 <code>mousedown</code>、<code>touchstart</code> 事件，我们可以收集用户每一次点击区域的大小，点击坐标在整个页面中的具体位置，点击元素的内容等信息。</p><pre><code class="language-js">export default function onClick() {
    ['mousedown', 'touchstart'].forEach(eventType =&gt; {
        let timer
        window.addEventListener(eventType, event =&gt; {
            clearTimeout(timer)
            timer = setTimeout(() =&gt; {
                const target = event.target
                const { top, left } = target.getBoundingClientRect()
                
                lazyReportCache({
                    top,
                    left,
                    eventType,
                    pageHeight: document.documentElement.scrollHeight || document.body.scrollHeight,
                    scrollTop: document.documentElement.scrollTop || document.body.scrollTop,
                    type: 'behavior',
                    subType: 'click',
                    target: target.tagName,
                    paths: event.path?.map(item =&gt; item.tagName).filter(Boolean),
                    startTime: event.timeStamp,
                    pageURL: getPageURL(),
                    outerHTML: target.outerHTML,
                    innerHTML: target.innerHTML,
                    width: target.offsetWidth,
                    height: target.offsetHeight,
                    viewport: {
                        width: window.innerWidth,
                        height: window.innerHeight,
                    },
                    uuid: getUUID(),
                })
            }, 500)
        })
    })
}</code></pre><h3 id="--16">页面跳转</h3><p>利用 <code>addEventListener()</code> 监听 <code>popstate</code>、<code>hashchange</code> 页面跳转事件。需要注意的是调用<code>history.pushState()</code>或<code>history.replaceState()</code>不会触发<code>popstate</code>事件。只有在做出浏览器动作时，才会触发该事件，如用户点击浏览器的回退按钮（或者在Javascript代码中调用<code>history.back()</code>或者<code>history.forward()</code>方法）。同理，<code>hashchange</code> 也一样。</p><pre><code class="language-js">export default function pageChange() {
    let from = ''
    window.addEventListener('popstate', () =&gt; {
        const to = getPageURL()

        lazyReportCache({
            from,
            to,
            type: 'behavior',
            subType: 'popstate',
            startTime: performance.now(),
            uuid: getUUID(),
        })

        from = to
    }, true)

    let oldURL = ''
    window.addEventListener('hashchange', event =&gt; {
        const newURL = event.newURL

        lazyReportCache({
            from: oldURL,
            to: newURL,
            type: 'behavior',
            subType: 'hashchange',
            startTime: performance.now(),
            uuid: getUUID(),
        })

        oldURL = newURL
    }, true)
}</code></pre><h3 id="vue--2">Vue 路由变更</h3><p>Vue 可以利用 <code>router.beforeEach</code> 钩子进行路由变更的监听。</p><pre><code class="language-js">export default function onVueRouter(router) {
    router.beforeEach((to, from, next) =&gt; {
        // 首次加载页面不用统计
        if (!from.name) {
            return next()
        }

        const data = {
            params: to.params,
            query: to.query,
        }

        lazyReportCache({
            data,
            name: to.name || to.path,
            type: 'behavior',
            subType: ['vue-router-change', 'pv'],
            startTime: performance.now(),
            from: from.fullPath,
            to: to.fullPath,
            uuid: getUUID(),
        })

        next()
    })
}</code></pre><h2 id="--17">数据上报</h2><h3 id="--18">上报方法</h3><p>数据上报可以使用以下几种方式：</p><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></li><li>image</li></ul><p>我写的简易 SDK 采用的是第一、第二种方式相结合的方式进行上报。利用 sendBeacon 来进行上报的优势非常明显。</p><p>使用 <strong><code>sendBeacon()</code></strong> 方法会使用户代理在有机会时异步地向服务器发送数据，同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题：数据可靠，传输异步并且不会影响下一页面的加载。</p><p>在不支持 sendBeacon 的浏览器下我们可以使用 XMLHttpRequest 来进行上报。一个 HTTP 请求包含发送和接收两个步骤。其实对于上报来说，我们只要确保能发出去就可以了。也就是发送成功了就行，接不接收响应无所谓。为此，我做了个实验，在 beforeunload 用 XMLHttpRequest 传送了 30kb 的数据（一般的待上报数据很少会有这么大），换了不同的浏览器，都可以成功发出去。当然，这和硬件性能、网络状态也是有关联的。</p><h3 id="--19">上报时机</h3><p>上报时机有三种：</p><ol><li>采用 <code>requestIdleCallback/setTimeout</code> 延时上报。</li><li>在 beforeunload 回调函数里上报。</li><li>缓存上报数据，达到一定数量后再上报。</li></ol><p>建议将三种方式结合一起上报：</p><ol><li>先缓存上报数据，缓存到一定数量后，利用 <code>requestIdleCallback/setTimeout</code> 延时上报。</li><li>在页面离开时统一将未上报的数据进行上报。</li></ol><h2 id="--20">总结</h2><p>仅看理论知识是比较难以理解的，为此我结合本文所讲的技术要点写了一个简单的<a href="https://github.com/woai3c/monitor-demo">监控 SDK</a>，可以用它来写一些简单的 DEMO，帮助加深理解。再结合本文一起阅读，效果更好。</p><h2 id="--21">参考资料</h2><h3 id="--22">性能监控</h3><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Performance_API">Performance API</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceResourceTiming">PerformanceResourceTiming</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API">Using_the_Resource_Timing_API</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming">PerformanceTiming</a></li><li><a href="https://web.dev/metrics/">Metrics</a></li><li><a href="https://web.dev/evolving-cls/">evolving-cls</a></li><li><a href="https://web.dev/custom-metrics/">custom-metrics</a></li><li><a href="https://github.com/GoogleChrome/web-vitals">web-vitals</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver">PerformanceObserver</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element_timing_API">Element_timing_API</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming">PerformanceEventTiming</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Timing-Allow-Origin">Timing-Allow-Origin</a></li><li><a href="https://web.dev/bfcache/">bfcache</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver">MutationObserver</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></li><li><a href="https://zhuanlan.zhihu.com/p/39292837">如何监控网页的卡顿</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></li></ul><h3 id="--23">错误监控</h3><ul><li><a href="https://github.com/joeyguo/noerror">noerror</a></li><li><a href="https://github.com/mozilla/source-map">source-map</a></li></ul><h3 id="--24">行为监控</h3><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event">popstate</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/hashchange_event">hashchange</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Linux/MacOS 上管理 SDK 的最正确方式 ]]>
                </title>
                <description>
                    <![CDATA[ 对开发者来说，就算是同一种语言，我们也可能会在电脑上装数个不同版本的环境，以应对一些历史包袱和复杂多变的客户环境。 所以长远来看，对待任何一门语言的SDK环境，我们都应找到一种方式，合理管理不同的SDK版本。这里我强烈建议你在处理自己电脑的开发环境时，有意识地对SDK版本进行管理，而不是直接上网搜了个最新版的安装包，莫名其妙地装，莫名其妙地用。 别担心，这并不难。各路大神已经把轮子给你造好了。 几乎对任何一门语言，只要你在网上搜索： xxxLanguage version manager 稍加甄别就能得到自己想要的答案。当然，下面我也把常用的一些语言及环境的version manager列了出来，供你享用。  * Java——sdkman [https://sdkman.io]👍  * Java——jabba [https://github.com/shyiko/jabba]  * Python——Miniforge [https://github.com/conda-forge/miniforge]👍  * Python——Anaconda [https://www ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/sdk-version-manager/</link>
                <guid isPermaLink="false">604b18f86ce45b059394b81c</guid>
                
                    <category>
                        <![CDATA[ SDK ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mac ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 牧云踏歌 ]]>
                </dc:creator>
                <pubDate>Fri, 12 Mar 2021 07:58:47 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/03/--2021-03-12---3.45.31-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>对开发者来说，就算是同一种语言，我们也可能会在电脑上装数个不同版本的环境，以应对一些历史包袱和复杂多变的客户环境。</p>
<p>所以长远来看，对待任何一门语言的SDK环境，我们都应找到一种方式，合理管理不同的SDK版本。这里我强烈建议你在处理自己电脑的开发环境时，有意识地对SDK版本进行管理，而不是直接上网搜了个最新版的安装包，莫名其妙地装，莫名其妙地用。</p>
<p>别担心，这并不难。各路大神已经把轮子给你造好了。</p>
<p>几乎对任何一门语言，只要你在网上搜索：</p>
<pre><code>xxxLanguage version manager
</code></pre>
<p>稍加甄别就能得到自己想要的答案。当然，下面我也把常用的一些语言及环境的<code>version manager</code>列了出来，供你享用。</p>
<ul>
<li>Java——<a href="https://sdkman.io">sdkman</a>👍</li>
<li>Java——<a href="https://github.com/shyiko/jabba">jabba</a></li>
<li>Python——<a href="https://github.com/conda-forge/miniforge">Miniforge</a>👍</li>
<li>Python——<a href="https://www.anaconda.com">Anaconda</a></li>
<li>Python——<a href="https://github.com/pyenv/pyenv">pyenv</a></li>
<li>Node——<a href="https://github.com/nvm-sh/nvm">nvm</a></li>
<li>Go——<a href="https://github.com/moovweb/gvm">gvm</a></li>
<li>Ruby——<a href="https://github.com/rvm/rvm">rvm</a></li>
<li>PHP——<a href="https://github.com/phpbrew/phpbrew">phpbrew</a></li>
</ul>
<p>这些工具的使用大同小异，咱们前后端各选一个代表测试一下，下面先来演示<code>node</code>的管理环境<code>nvm</code>。</p>
<h3 id="">安装</h3>
<p>直接在终端里执行这句话即可：</p>
<pre><code class="language-sh">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
</code></pre>
<h3 id="">使用</h3>
<p>配置完毕后，打开一个新的终端，可以通过<code>nvm ls-remote</code>列出所有可用的<code>node</code>版本了，如果只想查看<code>lts</code>版本，可以通过追加参数实现，<code>nvm ls-remote --lts</code>。确认要安装的版本后，就可以通过<code>nvm install v15.11.0</code>的方式安装了。如果你安装了多个版本，需要在不同版本间切换，只需要简单的使用<code>nvm use v15.11.0</code>就可以进行版本切换了。</p>
<h3 id="">更多</h3>
<p>关于<code>nvm</code>的使用，还有很多功能有待你来发掘，具体可以执行<code>nvm --help</code>获得更多有用信息。顺便一提，用<code>nvm</code>在<code>ARM</code>架构的<code>Mac</code>电脑上安装<code>node</code>的时候，是基于<code>node</code>的源码进行本地编译安装的，因而你将获得一个<code>ARM</code>版本的<code>node</code>，这点对正在使用<code>M1</code>芯片<code>Mac</code>的小伙伴非常友好。</p>
<p>介绍完了前端，咱们再来看看后端，这里以<code>Java</code>的<code>sdkman</code>进行演示。</p>
<h3 id="">安装</h3>
<p>也是非常简单，一句话的事。</p>
<pre><code>curl -s "https://get.sdkman.io" | bash
</code></pre>
<h3 id="">使用</h3>
<p>列出可用的<code>Java（JDK）</code>版本，执行<code>sdk list java</code>即可，然后通过执行<code>sdk install java 15.0.2-zulu</code>就可以安装你需要的<code>JDK</code>版本了。</p>
<p>额外说一句，如果是<code>ARM</code>架构的<code>Mac</code>其实是支持<code>x86</code>和<code>ARM</code>两种架构的<code>JDK</code>的，只是前者要经过<code>MacOS</code>的<code>Rosetta2</code>的转译，大部分情况下我们为了追求性能想使用<code>ARM</code>版本的<code>JDK</code>，此时你可以更改<code>sdkman</code>的配置文件，<code>vim ~/.sdkman/etc/config</code>，找到<code>sdkman_rosetta2_compatible</code>那一行，如果设置成<code>true</code>，就仅显示<code>ARM</code>版本的<code>JDK</code>了。</p>
<p>如果你安装了多个版本的<code>JDK</code>，一样可以通过<code>sdk use java 8.0.275-amzn</code>的方式进行切换。如果你想改变全局环境的默认<code>JDK</code>版本，则可以使用<code>sdk default java 8.0.275-amzn</code>进行设置。</p>
<h3 id="">其他</h3>
<p><code>sdkman</code>除了支持<code>JDK</code>的版本管理外，还支持<code>Java</code>系的很多技术栈，比如：<code>Gradle</code>、<code>Maven</code>、<code>Kotlin</code>、<code>Groovy</code>等。所以如果你有这方面需求的话，记得执行<code>sdk --help</code>探索更多有用功能哦。</p>
<h3 id="">八卦</h3>
<p>其实关于<code>sdkman</code>还有一段有意思的往事，曾经<code>sdkman</code>的名字是<code>gvm</code>，专门用来管理一门自<code>2003</code>年就存在的<code>Groovy</code>语言，这门语言跟<code>Java</code>一样，也是运行在<code>JVM</code>（Java虚拟机）上的。</p>
<p>但是后来程序界又迎来了另一个<code>G</code>姓语言——<code>Go</code>，越来越多的小伙伴希望<code>gvm</code>这个名字能见文知意，用作<code>Go</code>的版本管理，而不是去管理一个小众的<code>JVM</code>语言。</p>
<p>于是在2015年，之前维护<code>gvm</code>的小伙伴，宣布了<code>gvm</code>改名为<code>sdkman</code>，从而把<code>gvm</code>的使用权交给了<code>Go Version Manager</code>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
