<?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[ Gordan Tan - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Gordan Tan - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 15 May 2026 17:22:57 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/woai3c/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ The Front-End Monitoring Handbook: Track Performance, Errors, and User Behavior ]]>
                </title>
                <description>
                    <![CDATA[ A complete frontend monitoring system is essential for tracking application performance, errors, and user behavior. It consists of three main components: data collection and reporting, data processing and storage, and data visualization. This article... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-front-end-monitoring-handbook/</link>
                <guid isPermaLink="false">683e5606b7c8b1ebd3a32964</guid>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ frontend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gordan Tan ]]>
                </dc:creator>
                <pubDate>Tue, 03 Jun 2025 01:55:18 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748915696356/6e6edeed-2f41-40f9-97d6-8cc8686c3b25.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A complete frontend monitoring system is essential for tracking application performance, errors, and user behavior. It consists of three main components: data collection and reporting, data processing and storage, and data visualization.</p>
<p>This article focuses specifically on the first component – data collection and reporting – and shows you how to build a monitoring SDK from scratch. By the end of this article, you'll understand how to gather critical metrics about your application's performance, capture errors, track user behavior, and implement efficient reporting mechanisms.</p>
<p>Below is an outline of the topics we'll cover:</p>
<pre><code class="lang-javascript">                       ┌────────────────────┐
                       │  Data Collection   │
                       └──────────┬─────────┘
                                  │
         ┌─────────────────┬──────┴──────────────┐
         │                 │                     │
┌────────┴────────┐ ┌──────┴──────┐     ┌────────┴────────┐
│ <span class="hljs-built_in">Error</span> Monitoring │ │ Performance  │     │ Behavior       │
│                  │ │ Monitoring   │     │ Monitoring     │
└────────┬─────────┘ └──────┬──────┘     └────────┬────────┘
         │                  │                     │
┌────────┴────────┐ ┌──────┴──────────┐  ┌────────┴────────┐
│                 │ │                 │  │                 │
│ Resource Loading│ │ Resource Loading│  │     UV, PV      │
│     Errors      │ │      Time       │  │                 │
│                 │ │                 │  │  Page Access    │
│   JS Errors     │ │  API Request    │  │     Depth       │
│                 │ │     Time        │  │                 │
│ <span class="hljs-built_in">Promise</span> Errors  │ │                 │  │   Page Stay     │
│                 │ │   DNS, TCP,     │  │    Duration     │
│ Custom Errors   │ │ First-byte Time │  │                 │
│                 │ │                 │  │  Custom Event   │
│                 │ │   FPS Rate      │  │    Tracking     │
│                 │ │                 │  │                 │
│                 │ │ Cache Hit Rate  │  │   User Clicks   │
│                 │ │                 │  │                 │
│                 │ │  First Screen   │  │ Page Navigation │
│                 │ │  Render Time    │  │                 │
│                 │ │                 │  └─────────────────┘
│                 │ │  FP, FCP, LCP,  │
│                 │ │   FID, LCS,     │
│                 │ │ DOMContentLoaded│
│                 │ │    onload       │
└─────────────────┘ └─────────────────┘
</code></pre>
<p>Once data is collected, it needs to be reported to your backend systems for processing and analysis:</p>
<pre><code class="lang-javascript">                  ┌─────────────────┐
                  │ Data Reporting  │
                  └────────┬────────┘
                           │
          ┌────────────────┴────────────────┐
          │                                 │
┌─────────────────────┐           ┌─────────────────────┐
│  Reporting Methods  │           │  Reporting Timing   │
└──────────┬──────────┘           └──────────┬──────────┘
           │                                 │
     ┌─────┼─────┐               ┌───────────┼───────────┐
     │     │     │               │           │           │
┌────┴───┐ │ ┌───┴────┐ ┌────────┴────────┐ │ ┌─────────┴─────────┐
│  xhr   │ │ │ image  │ │ requestIdle     │ │ │ Upload when cache │
└────────┘ │ └────────┘ │ Callback/       │ │ │ limit is reached  │
           │            │ <span class="hljs-built_in">setTimeout</span>      │ │ └───────────────────┘
     ┌─────┴─────┐      └─────────────────┘ │
     │ sendBeacon│                          │
     └───────────┘                ┌─────────┴──────────┐
                                  │    beforeunload    │
                                  └────────────────────┘
</code></pre>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before diving into this tutorial, you should have:</p>
<ul>
<li><p>Basic knowledge of JavaScript and web development</p>
</li>
<li><p>Familiarity with browser APIs and event handling</p>
</li>
<li><p>Understanding of asynchronous programming concepts</p>
</li>
<li><p>Some experience with performance optimization concepts</p>
</li>
</ul>
<p>Since theoretical knowledge alone can be difficult to grasp, I've created a simple <a target="_blank" href="https://github.com/woai3c/monitor-demo">monitoring SDK</a> that implements these technical concepts. You can use it to create simple demos and gain a better understanding. Reading this article while experimenting with the SDK will provide the best learning experience.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-collect-performance-data">Collect Performance Data</a></p>
<ul>
<li><p><a class="post-section-overview" href="##heading-fp-first-paint">FP (First Paint)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fcp-first-contentful-paint">FCP (First Contentful Paint)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lcp-largest-contentful-paint">LCP (Largest Contentful Paint)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cls-cumulative-layout-shift">CLS (Cumulative Layout Shift)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-domcontentloaded-and-load-events">DOMContentLoaded and Load Events</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-first-screen-rendering-time">First Screen Rendering Time</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-api-request-timing">API Request Timing</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-resource-loading-time-and-cache-hit-rate">Resource Loading Time and Cache Hit Rate</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-browser-backforward-cache-bfc">Browser Back/Forward Cache</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fps">FPS</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vue-router-change-rendering-time">Vue Router Change Rendering Time</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-error-data-collection">Error Data Collection</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-resource-loading-errors">Resource Loading Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-javascript-errors">JavaScript Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-promise-errors">Promise Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sourcemap">Sourcemap</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vue-errors">Vue Errors</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-behavior-data-collection">Behavior Data Collection</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-pv-and-uv">PV and UV</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-page-stay-duration">Page Stay Duration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-page-access-depth">Page Access Depth</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-user-clicks">User Clicks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-page-navigation">Page Navigation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-vue-router-changes">Vue Router Changes</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-data-reporting">Data Reporting</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-reporting-methods">Reporting Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reporting-timing">Reporting Timing</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-performance-monitoring">Performance Monitoring</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-error-monitoring">Error Monitoring</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-behavior-monitoring">Behavior Monitoring</a></p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-collect-performance-data">Collect Performance Data</h2>
<p>Monitoring performance is crucial for providing users with a smooth, responsive experience. Slow websites lead to higher bounce rates and reduced conversions. By collecting performance metrics, you can identify bottlenecks, optimize critical rendering paths, and improve overall user satisfaction.</p>
<p>The Chrome developer team has proposed a series of metrics to monitor page performance, each measuring a different aspect of the user experience:</p>
<ul>
<li><p><strong>FP (First Paint)</strong> – Time from when the page starts loading until the first pixel is painted on the screen (essentially the white screen time)</p>
</li>
<li><p><strong>FCP (First Contentful Paint)</strong> – Time from page load start until any part of page content is rendered</p>
</li>
<li><p><strong>LCP (Largest Contentful Paint)</strong> – Time from page load start until the largest text block or image element completes rendering</p>
</li>
<li><p><strong>CLS (Cumulative Layout Shift)</strong> – Cumulative score of all unexpected layout shifts occurring between page load start and when the <a target="_blank" href="https://developer.chrome.com/docs/web-platform/page-lifecycle-api">page's lifecycle state</a> becomes hidden</p>
</li>
</ul>
<p>We can obtain these four performance metrics through <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver">PerformanceObserver</a> (they can also be retrieved via <code>performance.getEntriesByName()</code>, but this method doesn't provide real-time notifications when events occur). PerformanceObserver is a performance monitoring interface used to observe performance measurement events.</p>
<p>Let's examine each of these metrics in detail and see how to implement them in our SDK.</p>
<h3 id="heading-fp-first-paint">FP (First Paint)</h3>
<p>First Paint (FP) marks the point when the browser renders anything visually different from what was on the screen before navigation. This could be a background color change or any visual element that indicates to the user that the page is loading.</p>
<p>Implementation code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {        
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-keyword">if</span> (entry.name === <span class="hljs-string">'first-paint'</span>) {
            observer.disconnect()
        }
        <span class="hljs-built_in">console</span>.log(entry)
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
<span class="hljs-comment">// The buffered property indicates whether to observe cached data, </span>
<span class="hljs-comment">// allowing observation even if the monitoring code is added after the event occurs</span>
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'paint'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>This code creates a new PerformanceObserver that watches for 'paint' type events. When the first-paint event occurs, it logs the entry information and disconnects the observer since we only need to capture this event once per page load. The observer's <code>observe()</code> method is configured with <code>buffered: true</code> to ensure we can catch paint events that occurred before our code runs.</p>
<p>The FP measurement output:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">entryType</span>: <span class="hljs-string">"paint"</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">"first-paint"</span>,
    <span class="hljs-attr">startTime</span>: <span class="hljs-number">359</span>, <span class="hljs-comment">// FP time</span>
}
</code></pre>
<p>The <code>startTime</code> value represents the paint timing we need. This value (359ms in this example) tells us how long it took from the start of navigation until the first visual change appeared on screen. You can use this metric to optimize your critical rendering path and reduce the time users spend looking at a blank screen.</p>
<h3 id="heading-fcp-first-contentful-paint">FCP (First Contentful Paint)</h3>
<p>FCP (First Contentful Paint) refers to the time from page load start until any part of page content is rendered. The "content" in this metric refers to text, images (including background images), <code>&lt;svg&gt;</code> elements, and non-white <code>&lt;canvas&gt;</code> elements.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/8a8b6762583fbb357b8ac6488a1f972e583bab8e6dfaab599e9a5ca3ea5e2403/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f61346631633962363130323934343864616532623163666235376234656637357e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/8a8b6762583fbb357b8ac6488a1f972e583bab8e6dfaab599e9a5ca3ea5e2403/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f61346631633962363130323934343864616532623163666235376234656637357e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="FCP visualization showing content being painted on screen" width="600" height="400" loading="lazy"></a></p>
<p>To provide a good user experience, the FCP score should be kept under 1.8 seconds.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/5a0734e52cbed48e8639fe185204c237fd658e5d60560b18578c848d74dac12c/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f39383138633636383739623334356533623438343566663366653031653863397e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/5a0734e52cbed48e8639fe185204c237fd658e5d60560b18578c848d74dac12c/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f39383138633636383739623334356533623438343566663366653031653863397e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="FCP scoring scale: Good (0-1.8s), Needs Improvement (1.8-3s), Poor (3s+)" width="600" height="400" loading="lazy"></a></p>
<p>The measurement code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {        
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-keyword">if</span> (entry.name === <span class="hljs-string">'first-contentful-paint'</span>) {
            observer.disconnect()
        }

        <span class="hljs-built_in">console</span>.log(entry)
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'paint'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>We can get the value of FCP via the above code:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">entryType</span>: <span class="hljs-string">"paint"</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">"first-contentful-paint"</span>,
    <span class="hljs-attr">startTime</span>: <span class="hljs-number">459</span>, <span class="hljs-comment">// fcp 时间</span>
}
</code></pre>
<p>The <code>startTime</code> value is the painting time we need.</p>
<h3 id="heading-lcp-largest-contentful-paint">LCP (Largest Contentful Paint)</h3>
<p>LCP (Largest Contentful Paint) refers to the time from page load start until the largest text block or image element completes rendering. The LCP metric reports the relative render time of the largest visible image or text block in the viewport, measured from when the page first begins loading.</p>
<p>A good LCP score should be kept under 2.5 seconds.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/76d0f2b9a24d36f12714e9ce39a61ce426eda6ae087c643745c71337352d5c27/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63303930646438623034326334366432616461626135333935636136386634377e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/76d0f2b9a24d36f12714e9ce39a61ce426eda6ae087c643745c71337352d5c27/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63303930646438623034326334366432616461626135333935636136386634377e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="LCP scoring scale: Good (0-2.5s), Needs Improvement (2.5-4s), Poor (4s+)" width="600" height="400" loading="lazy"></a></p>
<p>The measurement code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (observer) {
        observer.disconnect()
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-built_in">console</span>.log(entry)
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'largest-contentful-paint'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>We can get the value of LCP via the above code:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">element</span>: p,
    <span class="hljs-attr">entryType</span>: <span class="hljs-string">"largest-contentful-paint"</span>,
    <span class="hljs-attr">id</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">loadTime</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">renderTime</span>: <span class="hljs-number">1021.299</span>,
    <span class="hljs-attr">size</span>: <span class="hljs-number">37932</span>,
    <span class="hljs-attr">startTime</span>: <span class="hljs-number">1021.299</span>,
    <span class="hljs-attr">url</span>: <span class="hljs-string">""</span>,
}
</code></pre>
<p>The <code>startTime</code> value is the painting time we need. And <code>element</code> refers to the element being painted during LCP.</p>
<p>The difference between FCP and LCP is: FCP event occurs when any content is painted, while LCP event occurs when the largest content finishes rendering.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/ed1fac2eb0ad92326cb76a6f71f7c661164d881fe13f18e8de9a1d7f9423ad53/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653634363337616339643234336135383130316438656430316665383836657e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/ed1fac2eb0ad92326cb76a6f71f7c661164d881fe13f18e8de9a1d7f9423ad53/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653634363337616339643234336135383130316438656430316665383836657e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Comparison of FCP and LCP timing on webpage loading timeline" width="600" height="400" loading="lazy"></a></p>
<p>LCP considers these elements:</p>
<ul>
<li><p><code>&lt;img&gt;</code> elements</p>
</li>
<li><p><code>&lt;image&gt;</code> elements inside <code>&lt;svg&gt;</code></p>
</li>
<li><p><code>&lt;video&gt;</code> elements (using poster images)</p>
</li>
<li><p>Elements with background images loaded via the <a target="_blank" href="https://developer.mozilla.org/docs/Web/CSS/url\(\)"><code>url()</code></a> function (not using <a target="_blank" href="https://developer.mozilla.org/docs/Web/CSS/CSS_Images/Using_CSS_gradients">CSS gradients</a>)</p>
</li>
<li><p>Block-level elements containing text nodes or other inline-level text elements</p>
</li>
</ul>
<h3 id="heading-cls-cumulative-layout-shift">CLS (Cumulative Layout Shift)</h3>
<p>CLS (Cumulative Layout Shift) refers to the cumulative score of all unexpected layout shifts occurring between page load start and when the <a target="_blank" href="https://developer.chrome.com/docs/web-platform/page-lifecycle-api">page's lifecycle state</a> becomes hidden.</p>
<p>An "unexpected layout shift" occurs when elements on a page move around without user interaction. Common examples include:</p>
<ul>
<li><p>A banner or ad suddenly appearing at the top of the page, pushing content down</p>
</li>
<li><p>A font loading and changing the size of text</p>
</li>
<li><p>An image loading without predefined dimensions, expanding and pushing other content out of the way</p>
</li>
<li><p>A button appearing below where a user is about to click, causing them to click the wrong element</p>
</li>
</ul>
<p>These shifts are frustrating for users and lead to accidental clicks, lost reading position, and overall poor user experience. CLS helps quantify this problem so you can identify and fix problematic elements.</p>
<p>The layout shift score is calculated as follows:</p>
<pre><code class="lang-javascript">layout shift score = impact score × distance score
</code></pre>
<p>The <a target="_blank" href="https://github.com/WICG/layout-instability#Impact-Fraction">impact score</a> measures how <em>unstable elements</em> affect the visible area between two frames. The <em>distance score</em> is calculated by taking the greatest distance any <em>unstable element</em> has moved (either horizontally or vertically) and dividing it by the viewport's largest dimension (width or height, whichever is greater).</p>
<p><strong>CLS is the sum of all layout shift scores.</strong></p>
<p>A layout shift occurs when a DOM element changes position between two rendered frames, as shown below:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/d70250d691a70bb776e1b6748c2b39c7c8ebf17a5e0299bc0bebe064a4d44d91/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f66663037643431633632343234386131623636633537363166303438326632637e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/d70250d691a70bb776e1b6748c2b39c7c8ebf17a5e0299bc0bebe064a4d44d91/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f66663037643431633632343234386131623636633537363166303438326632637e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Layout shift visualization showing element position change" width="600" height="400" loading="lazy"></a></p>
<p><a target="_blank" href="https://camo.githubusercontent.com/826f631d4e71a8eb8f53738821f788c864988cff7fae350b77a43b3b3c22d331/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f64306435616238313030633934383961393931646430626538653139386166307e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/826f631d4e71a8eb8f53738821f788c864988cff7fae350b77a43b3b3c22d331/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f64306435616238313030633934383961393931646430626538653139386166307e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Rectangle movement illustration demonstrating layout shift from top-left to right" width="600" height="400" loading="lazy"></a></p>
<p>In the above diagram, the rectangle moves from the top-left to the right side, counting as one layout shift. In CLS terminology, there's a concept called "session window": one or more individual layout shifts occurring in rapid succession, with less than 1 second between each shift and a maximum window duration of 5 seconds.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/e2a49800b87d502b7a81f69ebc4259c27ec3c101faac3a65df30cd080ec29a85/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63366166326563353639363434303133393632363435383230656662313664337e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/e2a49800b87d502b7a81f69ebc4259c27ec3c101faac3a65df30cd080ec29a85/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f63366166326563353639363434303133393632363435383230656662313664337e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Session window concept showing multiple layout shifts grouped within time constraints" width="600" height="400" loading="lazy"></a></p>
<p>For example, in the second session window shown above, there are four layout shifts. Each shift must occur less than 1 second after the previous one, and the time between the first and last shifts must not exceed 5 seconds to qualify as a session window. If these conditions aren't met, it's considered a new session window. This specification comes from extensive experimentation and research by the Chrome team, as detailed in <a target="_blank" href="https://web.dev/blog/evolving-cls">Evolving the CLS metric</a>.</p>
<p>CLS has three calculation methods:</p>
<ol>
<li><p>Cumulative</p>
</li>
<li><p>Average of all session windows</p>
</li>
<li><p>Maximum of all session windows</p>
</li>
</ol>
<h4 id="heading-cumulative"><strong>Cumulative</strong></h4>
<p>This method adds up all layout shift scores from page load start. However, this approach disadvantages long-lived pages - the longer a page is open, the higher the CLS score becomes.</p>
<h4 id="heading-average-of-all-session-windows"><strong>Average of All Session Windows</strong></h4>
<p>This method calculates based on session windows rather than individual layout shifts, taking the average of all session window scores. However, this approach has limitations.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/b4e2797c9a0b1de2c2f0c291374a53cfcb6b5d84c79c4d045ce2a78de1ed22fc/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f34326535323038643833663334396462383463663461323731393461353766327e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/b4e2797c9a0b1de2c2f0c291374a53cfcb6b5d84c79c4d045ce2a78de1ed22fc/68747470733a2f2f70362d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f34326535323038643833663334396462383463663461323731393461353766327e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Comparison of session windows with different CLS scores showing averaging limitations" width="600" height="400" loading="lazy"></a></p>
<p>As shown above, if the first session window has a high CLS score and the second has a low score, averaging them masks the actual page behavior. The average doesn't reflect that the page had more shifts early on and fewer later.</p>
<h4 id="heading-maximum-of-all-session-windows"><strong>Maximum of All Session Windows</strong></h4>
<p>This is currently the optimal calculation method, using the highest session window score to reflect the worst-case scenario for layout shifts. For more details, see <a target="_blank" href="https://web.dev/blog/evolving-cls">Evolving the CLS metric</a>.</p>
<p>Below is the implementation code for the third calculation method:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> sessionValue = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> sessionEntries = []
<span class="hljs-keyword">const</span> cls = {
    <span class="hljs-attr">subType</span>: <span class="hljs-string">'layout-shift'</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">'layout-shift'</span>,
    <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
    <span class="hljs-attr">pageURL</span>: getPageURL(),
    <span class="hljs-attr">value</span>: <span class="hljs-number">0</span>,
}

<span class="hljs-keyword">const</span> entryHandler = <span class="hljs-function">(<span class="hljs-params">list</span>) =&gt;</span> {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> list.getEntries()) {
        <span class="hljs-comment">// Only count layout shifts without recent user input.</span>
        <span class="hljs-keyword">if</span> (!entry.hadRecentInput) {
            <span class="hljs-keyword">const</span> firstSessionEntry = sessionEntries[<span class="hljs-number">0</span>]
            <span class="hljs-keyword">const</span> lastSessionEntry = sessionEntries[sessionEntries.length - <span class="hljs-number">1</span>]

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

            <span class="hljs-comment">// If the current session value is larger than the current CLS value,</span>
            <span class="hljs-comment">// update CLS and the entries contributing to it.</span>
            <span class="hljs-keyword">if</span> (sessionValue &gt; cls.value) {
                cls.value = sessionValue
                cls.entries = sessionEntries
                cls.startTime = performance.now()
                lazyReportCache(deepCopy(cls))
            }
        }
    }
}

<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> PerformanceObserver(entryHandler)
observer.observe({ <span class="hljs-attr">type</span>: <span class="hljs-string">'layout-shift'</span>, <span class="hljs-attr">buffered</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>A single layout shift measurement contains the following data:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-attr">duration</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">entryType</span>: <span class="hljs-string">"layout-shift"</span>,
  <span class="hljs-attr">hadRecentInput</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">lastInputTime</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">name</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">sources</span>: (<span class="hljs-number">2</span>) [LayoutShiftAttribution, LayoutShiftAttribution],
  <span class="hljs-attr">startTime</span>: <span class="hljs-number">1176.199999999255</span>,
  <span class="hljs-attr">value</span>: <span class="hljs-number">0.000005752046026677329</span>,
}
</code></pre>
<p>The <code>value</code> field represents the layout shift score.</p>
<h3 id="heading-domcontentloaded-and-load-events"><strong>DOMContentLoaded and Load Events</strong></h3>
<p>The <code>DOMContentLoaded</code> event is triggered when the HTML is fully loaded and parsed, without waiting for CSS, images, and iframes to load.</p>
<p>The <code>load</code> event is triggered when the entire page and all dependent resources such as stylesheets and images have finished loading.</p>
<p>Although these performance metrics are older, they still provide valuable insights into page behavior. Monitoring them remains necessary.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { lazyReportCache } <span class="hljs-keyword">from</span> <span class="hljs-string">'../utils/report'</span>

[<span class="hljs-string">'load'</span>, <span class="hljs-string">'DOMContentLoaded'</span>].forEach(<span class="hljs-function"><span class="hljs-params">type</span> =&gt;</span> onEvent(type))

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onEvent</span>(<span class="hljs-params">type</span>) </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">callback</span>(<span class="hljs-params"></span>) </span>{
        lazyReportCache({
            <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
            <span class="hljs-attr">subType</span>: type.toLocaleLowerCase(),
            <span class="hljs-attr">startTime</span>: performance.now(),
        })

        <span class="hljs-built_in">window</span>.removeEventListener(type, callback, <span class="hljs-literal">true</span>)
    }

    <span class="hljs-built_in">window</span>.addEventListener(type, callback, <span class="hljs-literal">true</span>)
}
</code></pre>
<h3 id="heading-first-screen-rendering-time"><strong>First Screen Rendering Time</strong></h3>
<p>In most cases, the first screen rendering time can be obtained through the <code>load</code> event. However, there are exceptions, such as asynchronously loaded images and DOM elements.</p>
<pre><code class="lang-javascript">&lt;script&gt;
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">document</span>.body.innerHTML = <span class="hljs-string">`
            &lt;div&gt;
                &lt;!-- lots of code... --&gt;
            &lt;/div&gt;
        `</span>
    }, <span class="hljs-number">3000</span>)
&lt;/script&gt;
</code></pre>
<p>In such cases, we cannot obtain the first screen rendering time through the <code>load</code> event. Instead, we need to use <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver">MutationObserver</a> to get the first screen rendering time. MutationObserver triggers events when the properties of the DOM elements it's monitoring change.</p>
<p>The process of calculating first screen rendering time:</p>
<ol>
<li><p>Use MutationObserver to monitor the document object, triggering events whenever DOM element properties change.</p>
</li>
<li><p>Check if the DOM element is in the first screen. If it is, call <code>performance.now()</code> in the <code>requestAnimationFrame()</code> callback function to get the current time as its rendering time.</p>
</li>
<li><p>Compare the rendering time of the last DOM element with the loading time of all images in the first screen, and use the maximum value as the first screen rendering time.</p>
</li>
</ol>
<h4 id="heading-monitoring-dom"><strong>Monitoring DOM</strong></h4>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> next = <span class="hljs-built_in">window</span>.requestAnimationFrame ? requestAnimationFrame : <span class="hljs-built_in">setTimeout</span>
<span class="hljs-keyword">const</span> ignoreDOMList = [<span class="hljs-string">'STYLE'</span>, <span class="hljs-string">'SCRIPT'</span>, <span class="hljs-string">'LINK'</span>]

observer = <span class="hljs-keyword">new</span> MutationObserver(<span class="hljs-function"><span class="hljs-params">mutationList</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> entry = {
        <span class="hljs-attr">children</span>: [],
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> mutation <span class="hljs-keyword">of</span> mutationList) {
        <span class="hljs-keyword">if</span> (mutation.addedNodes.length &amp;&amp; isInScreen(mutation.target)) {
             <span class="hljs-comment">// ...</span>
        }
    }

    <span class="hljs-keyword">if</span> (entry.children.length) {
        entries.push(entry)
        next(<span class="hljs-function">() =&gt;</span> {
            entry.startTime = performance.now()
        })
    }
})

observer.observe(<span class="hljs-built_in">document</span>, {
    <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">subtree</span>: <span class="hljs-literal">true</span>,
})
</code></pre>
<p>The code above monitors DOM changes while filtering out <code>style</code>, <code>script</code>, and <code>link</code> tags.</p>
<h4 id="heading-checking-if-element-is-in-first-screen"><strong>Checking if Element is in First Screen</strong></h4>
<p>A page may have a lot of content, but users can only see one screen at a time. Therefore, when calculating first screen rendering time, we need to limit the scope to content visible in the current screen.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> viewportWidth = <span class="hljs-built_in">window</span>.innerWidth
<span class="hljs-keyword">const</span> viewportHeight = <span class="hljs-built_in">window</span>.innerHeight

<span class="hljs-comment">// Check if DOM element is in screen</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isInScreen</span>(<span class="hljs-params">dom</span>) </span>{
    <span class="hljs-keyword">const</span> rectInfo = dom.getBoundingClientRect()
    <span class="hljs-keyword">if</span> (
        rectInfo.left &gt;= <span class="hljs-number">0</span> 
        &amp;&amp; rectInfo.left &lt; viewportWidth
        &amp;&amp; rectInfo.top &gt;= <span class="hljs-number">0</span>
        &amp;&amp; rectInfo.top &lt; viewportHeight
    ) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
}
</code></pre>
<h4 id="heading-using-requestanimationframe-to-get-dom-rendering-time"><strong>Using</strong> <code>requestAnimationFrame()</code> to Get DOM Rendering Time</h4>
<p>When DOM changes trigger the MutationObserver event, it only means the DOM content can be read, not that it has been painted to the screen.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/1c118ccd17b38cf8054796f978f864bcb055b3db7659b9c4b84acc349c978531/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f36373233306335653538666634633639396265373735383635366534353034667e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/1c118ccd17b38cf8054796f978f864bcb055b3db7659b9c4b84acc349c978531/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f36373233306335653538666634633639396265373735383635366534353034667e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Browser rendering pipeline showing DOM content loaded but not yet painted" width="600" height="400" loading="lazy"></a></p>
<p>As shown in the image above, when the MutationObserver event is triggered, we can read that <code>document.body</code> already has content, but the left side of the screen hasn't painted anything yet. Therefore, we need to call <code>requestAnimationFrame()</code> to get the current time as the DOM rendering time after the browser has successfully painted.</p>
<h4 id="heading-comparing-with-all-image-loading-times-in-first-screen"><strong>Comparing with All Image Loading Times in First Screen</strong></h4>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getRenderTime</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> startTime = <span class="hljs-number">0</span>
    entries.forEach(<span class="hljs-function"><span class="hljs-params">entry</span> =&gt;</span> {
        <span class="hljs-keyword">if</span> (entry.startTime &gt; startTime) {
            startTime = entry.startTime
        }
    })

    <span class="hljs-comment">// Need to compare with all image loading times in current page, take the maximum</span>
    <span class="hljs-comment">// Image request time must be less than startTime, response end time must be greater than startTime</span>
    performance.getEntriesByType(<span class="hljs-string">'resource'</span>).forEach(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> {
        <span class="hljs-keyword">if</span> (
            item.initiatorType === <span class="hljs-string">'img'</span>
            &amp;&amp; item.fetchStart &lt; startTime 
            &amp;&amp; item.responseEnd &gt; startTime
        ) {
            startTime = item.responseEnd
        }
    })

    <span class="hljs-keyword">return</span> startTime
}
</code></pre>
<h4 id="heading-optimization"><strong>Optimization</strong></h4>
<p>The current code still needs optimization, with two main points to consider:</p>
<ol>
<li><p>When should we report the rendering time?</p>
</li>
<li><p>How to handle asynchronously added DOM elements?</p>
</li>
</ol>
<p>For the first point, we must report the rendering time after DOM changes stop, which typically happens after the load event triggers. Therefore, we can report at this point.</p>
<p>For the second point, we can report after the LCP event triggers. Whether DOM elements are loaded synchronously or asynchronously, they need to be painted, so we can monitor the LCP event and only allow reporting after it triggers.</p>
<p>Combining these two approaches, we get the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> isOnLoaded = <span class="hljs-literal">false</span>
executeAfterLoad(<span class="hljs-function">() =&gt;</span> {
    isOnLoaded = <span class="hljs-literal">true</span>
})

<span class="hljs-keyword">let</span> timer
<span class="hljs-keyword">let</span> observer
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkDOMChange</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">clearTimeout</span>(timer)
    timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">// Calculate first screen rendering time after load and LCP events trigger and DOM tree stops changing</span>
        <span class="hljs-keyword">if</span> (isOnLoaded &amp;&amp; isLCPDone()) {
            observer &amp;&amp; observer.disconnect()
            lazyReportCache({
                <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
                <span class="hljs-attr">subType</span>: <span class="hljs-string">'first-screen-paint'</span>,
                <span class="hljs-attr">startTime</span>: getRenderTime(),
                <span class="hljs-attr">pageURL</span>: getPageURL(),
            })

            entries = <span class="hljs-literal">null</span>
        } <span class="hljs-keyword">else</span> {
            checkDOMChange()
        }
    }, <span class="hljs-number">500</span>)
}
</code></pre>
<p>The <code>checkDOMChange()</code> function is called each time the MutationObserver event triggers and needs to be debounced.</p>
<h3 id="heading-api-request-timing"><strong>API Request Timing</strong></h3>
<p>To monitor API request timing, we need to intercept both XMLHttpRequest and fetch requests.</p>
<p><strong>Monitoring XMLHttpRequest</strong></p>
<pre><code class="lang-javascript">originalProto.open = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">newOpen</span>(<span class="hljs-params">...args</span>) </span>{
    <span class="hljs-built_in">this</span>.url = args[<span class="hljs-number">1</span>]
    <span class="hljs-built_in">this</span>.method = args[<span class="hljs-number">0</span>]
    originalOpen.apply(<span class="hljs-built_in">this</span>, args)
}

originalProto.send = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">newSend</span>(<span class="hljs-params">...args</span>) </span>{
    <span class="hljs-built_in">this</span>.startTime = <span class="hljs-built_in">Date</span>.now()

    <span class="hljs-keyword">const</span> onLoadend = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">this</span>.endTime = <span class="hljs-built_in">Date</span>.now()
        <span class="hljs-built_in">this</span>.duration = <span class="hljs-built_in">this</span>.endTime - <span class="hljs-built_in">this</span>.startTime

        <span class="hljs-keyword">const</span> { status, duration, startTime, endTime, url, method } = <span class="hljs-built_in">this</span>
        <span class="hljs-keyword">const</span> reportData = {
            status,
            duration,
            startTime,
            endTime,
            url,
            <span class="hljs-attr">method</span>: (method || <span class="hljs-string">'GET'</span>).toUpperCase(),
            <span class="hljs-attr">success</span>: status &gt;= <span class="hljs-number">200</span> &amp;&amp; status &lt; <span class="hljs-number">300</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'xhr'</span>,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
        }

        lazyReportCache(reportData)

        <span class="hljs-built_in">this</span>.removeEventListener(<span class="hljs-string">'loadend'</span>, onLoadend, <span class="hljs-literal">true</span>)
    }

    <span class="hljs-built_in">this</span>.addEventListener(<span class="hljs-string">'loadend'</span>, onLoadend, <span class="hljs-literal">true</span>)
    originalSend.apply(<span class="hljs-built_in">this</span>, args)
}
</code></pre>
<p>To determine if an XML request is successful, we can check if its status code is between 200 and 299. If it is, the request was successful; otherwise, it failed.</p>
<p><strong>Monitoring fetch</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> originalFetch = <span class="hljs-built_in">window</span>.fetch

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">overwriteFetch</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">window</span>.fetch = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">newFetch</span>(<span class="hljs-params">url, config</span>) </span>{
        <span class="hljs-keyword">const</span> startTime = <span class="hljs-built_in">Date</span>.now()
        <span class="hljs-keyword">const</span> reportData = {
            startTime,
            url,
            <span class="hljs-attr">method</span>: (config?.method || <span class="hljs-string">'GET'</span>).toUpperCase(),
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'fetch'</span>,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
        }

        <span class="hljs-keyword">return</span> originalFetch(url, config)
        .then(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> {
            reportData.endTime = <span class="hljs-built_in">Date</span>.now()
            reportData.duration = reportData.endTime - reportData.startTime

            <span class="hljs-keyword">const</span> data = res.clone()
            reportData.status = data.status
            reportData.success = data.ok

            lazyReportCache(reportData)

            <span class="hljs-keyword">return</span> res
        })
        .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
            reportData.endTime = <span class="hljs-built_in">Date</span>.now()
            reportData.duration = reportData.endTime - reportData.startTime
            reportData.status = <span class="hljs-number">0</span>
            reportData.success = <span class="hljs-literal">false</span>

            lazyReportCache(reportData)

            <span class="hljs-keyword">throw</span> err
        })
    }
}
</code></pre>
<p>For fetch requests, we can determine success by checking the <code>ok</code> field in the response data. If it's <code>true</code>, the request was successful; otherwise, it failed.</p>
<p><strong>Note</strong>: The API request timing we monitor may differ from what's shown in Chrome DevTools. This is because Chrome DevTools shows the time for the entire HTTP request and interface process. But XHR and fetch are asynchronous requests – after the interface request succeeds, the callback function needs to be called. When the event triggers, the callback function is placed in the message queue, and then the browser processes it. There's also a waiting period in between.</p>
<h3 id="heading-resource-loading-time-and-cache-hit-rate"><strong>Resource Loading Time and Cache Hit Rate</strong></h3>
<p>We can monitor <code>resource</code> and <code>navigation</code> events through <code>PerformanceObserver</code>. If the browser doesn't support <code>PerformanceObserver</code>, we can fall back to using <code>performance.getEntriesByType(entryType)</code>.</p>
<p>When the <code>resource</code> event triggers, we can get the corresponding resource list. Each resource object contains the following fields:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/cf1c72dd2f2e45626e1f601a11b5c1a9a829125d30bc291f4f2bedc8a7d2391c/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653663623330616539613434343762626534336266636666366336633461317e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/cf1c72dd2f2e45626e1f601a11b5c1a9a829125d30bc291f4f2bedc8a7d2391c/68747470733a2f2f70392d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f30653663623330616539613434343762626534336266636666366336633461317e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Resource object fields in PerformanceResourceTiming interface" width="600" height="400" loading="lazy"></a></p>
<p>From these fields, we can extract useful information:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">name</span>: entry.name, <span class="hljs-comment">// Resource name</span>
    <span class="hljs-attr">subType</span>: entryType,
    <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
    <span class="hljs-attr">sourceType</span>: entry.initiatorType, <span class="hljs-comment">// Resource type</span>
    <span class="hljs-attr">duration</span>: entry.duration, <span class="hljs-comment">// Resource loading duration</span>
    <span class="hljs-attr">dns</span>: entry.domainLookupEnd - entry.domainLookupStart, <span class="hljs-comment">// DNS duration</span>
    <span class="hljs-attr">tcp</span>: entry.connectEnd - entry.connectStart, <span class="hljs-comment">// TCP connection duration</span>
    <span class="hljs-attr">redirect</span>: entry.redirectEnd - entry.redirectStart, <span class="hljs-comment">// Redirect duration</span>
    <span class="hljs-attr">ttfb</span>: entry.responseStart, <span class="hljs-comment">// Time to first byte</span>
    <span class="hljs-attr">protocol</span>: entry.nextHopProtocol, <span class="hljs-comment">// Request protocol</span>
    <span class="hljs-attr">responseBodySize</span>: entry.encodedBodySize, <span class="hljs-comment">// Response body size</span>
    <span class="hljs-attr">responseHeaderSize</span>: entry.transferSize - entry.encodedBodySize, <span class="hljs-comment">// Response header size</span>
    <span class="hljs-attr">resourceSize</span>: entry.decodedBodySize, <span class="hljs-comment">// Resource size after decompression</span>
    <span class="hljs-attr">isCache</span>: isCache(entry), <span class="hljs-comment">// Whether cache was hit</span>
    <span class="hljs-attr">startTime</span>: performance.now(),
}
</code></pre>
<h4 id="heading-determining-if-resource-hit-cache">Determining if Resource Hit Cache</h4>
<p>Among these resource objects, there's a <code>transferSize</code> field that represents the size of the resource being fetched, including response header fields and response data size. If this value is 0, it means the resource was read directly from cache (forced cache). If this value is not 0 but the <code>encodedBodySize</code> field is 0, it means it used negotiated cache (<code>encodedBodySize</code> represents the size of the response data body).</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isCache</span>(<span class="hljs-params">entry</span>) </span>{
    <span class="hljs-comment">// Read directly from cache or 304</span>
    <span class="hljs-keyword">return</span> entry.transferSize === <span class="hljs-number">0</span> || (entry.transferSize !== <span class="hljs-number">0</span> &amp;&amp; entry.encodedBodySize === <span class="hljs-number">0</span>)
}
</code></pre>
<p>If it doesn't meet the above conditions, it means the cache was not hit. Then we can calculate the cache hit rate by dividing <code>all cached data/total data</code>.</p>
<h3 id="heading-browser-backforward-cache-bfc"><strong>Browser Back/Forward Cache (BFC)</strong></h3>
<p>BFC is a memory cache that saves the entire page in memory. When users navigate back, they can see the entire page immediately without refreshing. According to the article <a target="_blank" href="https://web.dev/bfcache/">bfcache</a>, Firefox and Safari have always supported BFC, while Chrome only supports it in high-version mobile browsers. But when I tested it, only Safari supported it – my Firefox version might have been different.</p>
<p>Still, BFC also has drawbacks. When users navigate back and restore the page from BFC, the original page's code won't execute again. For this reason, browsers provide a <code>pageshow</code> event where we can put code that needs to be executed again.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'pageshow'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">event</span>) </span>{
  <span class="hljs-comment">// If this property is true, it means the page was restored from BFC</span>
  <span class="hljs-keyword">if</span> (event.persisted) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This page was restored from the bfcache.'</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This page was loaded normally.'</span>);
  }
});
</code></pre>
<p>For pages restored from BFC, we also need to collect their FP, FCP, LCP, and other timing metrics.</p>
<pre><code class="lang-javascript">onBFCacheRestore(<span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    requestAnimationFrame(<span class="hljs-function">() =&gt;</span> {
        [<span class="hljs-string">'first-paint'</span>, <span class="hljs-string">'first-contentful-paint'</span>].forEach(<span class="hljs-function"><span class="hljs-params">type</span> =&gt;</span> {
            lazyReportCache({
                <span class="hljs-attr">startTime</span>: performance.now() - event.timeStamp,
                <span class="hljs-attr">name</span>: type,
                <span class="hljs-attr">subType</span>: type,
                <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
                <span class="hljs-attr">pageURL</span>: getPageURL(),
                <span class="hljs-attr">bfc</span>: <span class="hljs-literal">true</span>,
            })
        })
    })
})
</code></pre>
<p>The code above is easy to understand. After the <code>pageshow</code> event triggers, we subtract the event timestamp from the current time – this time difference is the rendering time of the performance metrics.</p>
<p><strong>Note</strong>: For pages restored from BFC, these performance metrics usually have very small values, around 10 ms. This means that we need to add an identifier field <code>bfc: true</code> so we can ignore them when doing performance statistics.</p>
<h3 id="heading-fps"><strong>FPS</strong></h3>
<p>We can calculate the current page's FPS using <code>requestAnimationFrame()</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> next = <span class="hljs-built_in">window</span>.requestAnimationFrame 
    ? requestAnimationFrame : <span class="hljs-function">(<span class="hljs-params">callback</span>) =&gt;</span> { <span class="hljs-built_in">setTimeout</span>(callback, <span class="hljs-number">1000</span> / <span class="hljs-number">60</span>) }

<span class="hljs-keyword">const</span> frames = []

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fps</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> frame = <span class="hljs-number">0</span>
    <span class="hljs-keyword">let</span> lastSecond = <span class="hljs-built_in">Date</span>.now()

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateFPS</span>(<span class="hljs-params"></span>) </span>{
        frame++
        <span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now()
        <span class="hljs-keyword">if</span> (lastSecond + <span class="hljs-number">1000</span> &lt;= now) {
            <span class="hljs-comment">// Since now - lastSecond is in milliseconds, frame needs to be multiplied by 1000</span>
            <span class="hljs-keyword">const</span> fps = <span class="hljs-built_in">Math</span>.round((frame * <span class="hljs-number">1000</span>) / (now - lastSecond))
            frames.push(fps)

            frame = <span class="hljs-number">0</span>
            lastSecond = now
        }

        <span class="hljs-comment">// Avoid reporting too frequently, cache a certain amount before reporting</span>
        <span class="hljs-keyword">if</span> (frames.length &gt;= <span class="hljs-number">60</span>) {
            report(deepCopy({
                frames,
                <span class="hljs-attr">type</span>: <span class="hljs-string">'performace'</span>,
                <span class="hljs-attr">subType</span>: <span class="hljs-string">'fps'</span>,
            }))

            frames.length = <span class="hljs-number">0</span>
        }

        next(calculateFPS)
    }

    calculateFPS()
}
</code></pre>
<p>The code logic is as follows:</p>
<p>First record an initial time, then each time <code>requestAnimationFrame()</code> triggers, increment the frame count by 1. After one second passes, we can get the current frame rate by dividing <code>frame count/elapsed time</code>.</p>
<p>When three consecutive FPS values below 20 appear, we can determine that the page has become unresponsive. This technique is based on the observation that smooth animations require at least 20 FPS to appear fluid to users.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isBlocking</span>(<span class="hljs-params">fpsList, below = <span class="hljs-number">20</span>, last = <span class="hljs-number">3</span></span>) </span>{
    <span class="hljs-keyword">let</span> count = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; fpsList.length; i++) {
        <span class="hljs-keyword">if</span> (fpsList[i] &amp;&amp; fpsList[i] &lt; below) {
            count++
        } <span class="hljs-keyword">else</span> {
            count = <span class="hljs-number">0</span>
        }

        <span class="hljs-keyword">if</span> (count &gt;= last) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}
</code></pre>
<h3 id="heading-vue-router-change-rendering-time"><strong>Vue Router Change Rendering Time</strong></h3>
<p>We already know how to calculate first screen rendering time, but how do we calculate the page rendering time caused by route changes in SPA applications? This article uses Vue as an example to explain my approach.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onVueRouter</span>(<span class="hljs-params">Vue, router</span>) </span>{
    <span class="hljs-keyword">let</span> isFirst = <span class="hljs-literal">true</span>
    <span class="hljs-keyword">let</span> startTime
    router.beforeEach(<span class="hljs-function">(<span class="hljs-params">to, <span class="hljs-keyword">from</span>, next</span>) =&gt;</span> {
        <span class="hljs-comment">// First page load already has other rendering time metrics available</span>
        <span class="hljs-keyword">if</span> (isFirst) {
            isFirst = <span class="hljs-literal">false</span>
            <span class="hljs-keyword">return</span> next()
        }

        <span class="hljs-comment">// Add a new field to router to indicate whether to calculate rendering time</span>
        <span class="hljs-comment">// Only needed for route changes</span>
        router.needCalculateRenderTime = <span class="hljs-literal">true</span>
        startTime = performance.now()

        next()
    })

    <span class="hljs-keyword">let</span> timer
    Vue.mixin({
        mounted() {
            <span class="hljs-keyword">if</span> (!router.needCalculateRenderTime) <span class="hljs-keyword">return</span>

            <span class="hljs-built_in">this</span>.$nextTick(<span class="hljs-function">() =&gt;</span> {
                <span class="hljs-comment">// Code that only runs after the entire view has been rendered</span>
                <span class="hljs-keyword">const</span> now = performance.now()
                <span class="hljs-built_in">clearTimeout</span>(timer)

                timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
                    router.needCalculateRenderTime = <span class="hljs-literal">false</span>
                    lazyReportCache({
                        <span class="hljs-attr">type</span>: <span class="hljs-string">'performance'</span>,
                        <span class="hljs-attr">subType</span>: <span class="hljs-string">'vue-router-change-paint'</span>,
                        <span class="hljs-attr">duration</span>: now - startTime,
                        <span class="hljs-attr">startTime</span>: now,
                        <span class="hljs-attr">pageURL</span>: getPageURL(),
                    })
                }, <span class="hljs-number">1000</span>)
            })
        },
    })
}
</code></pre>
<p>The code logic is as follows:</p>
<ol>
<li><p>Monitor route hooks – when route changes occur, the <code>router.beforeEach()</code> hook triggers. In this hook's callback function, record the current time as the rendering start time.</p>
</li>
<li><p>Use <code>Vue.mixin()</code> to inject a function into all components' <code>mounted()</code> hooks. Each function executes a debounced function.</p>
</li>
<li><p>When the last component's <code>mounted()</code> triggers, it means all components under this route have been mounted. We can get the rendering time in the <code>this.$nextTick()</code> callback function.</p>
</li>
</ol>
<p>Also, we need to consider another case. When not changing routes, there may also be component changes, in which case we shouldn't calculate rendering time in these components' <code>mounted()</code> hooks. Therefore, we need to add a <code>needCalculateRenderTime</code> field - set it to true when changing routes to indicate that rendering time can be calculated.</p>
<h2 id="heading-error-data-collection"><strong>Error Data Collection</strong></h2>
<p>Error monitoring is a critical aspect of frontend monitoring that helps identify issues users encounter while interacting with your application. By tracking and analyzing these errors, you can proactively fix bugs before they affect more users, improving both user experience and application reliability.</p>
<p>In this section, we'll explore how to capture various types of errors including resource loading failures, JavaScript runtime errors, unhandled promises, and framework-specific errors.</p>
<h3 id="heading-resource-loading-errors"><strong>Resource Loading Errors</strong></h3>
<p>Resource loading errors occur when the browser fails to load external resources like images, stylesheets, scripts, and fonts. These errors can significantly impact user experience by causing missing content, broken layouts, or even preventing core functionality from working.</p>
<p>Using <code>addEventListener()</code> to monitor the error event can capture resource loading failure errors.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Capture resource loading failure errors js css img...</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'error'</span>, <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> target = e.target
    <span class="hljs-keyword">if</span> (!target) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">if</span> (target.src || target.href) {
        <span class="hljs-keyword">const</span> url = target.src || target.href
        lazyReportCache({
            url,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'resource'</span>,
            <span class="hljs-attr">startTime</span>: e.timeStamp,
            <span class="hljs-attr">html</span>: target.outerHTML,
            <span class="hljs-attr">resourceType</span>: target.tagName,
            <span class="hljs-attr">paths</span>: e.path.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.tagName).filter(<span class="hljs-built_in">Boolean</span>),
            <span class="hljs-attr">pageURL</span>: getPageURL(),
        })
    }
}, <span class="hljs-literal">true</span>)
</code></pre>
<p>This code listens for the global <code>error</code> event with the capture option set to true, which allows it to catch errors from resource elements like <code>&lt;img&gt;</code>, <code>&lt;link&gt;</code>, and <code>&lt;script&gt;</code>. When a resource fails to load, it collects important information including:</p>
<ul>
<li><p>The URL of the failed resource</p>
</li>
<li><p>The element type (img, link, script)</p>
</li>
<li><p>The HTML of the element that failed</p>
</li>
<li><p>The DOM path to the element</p>
</li>
<li><p>The page URL where the error occurred</p>
</li>
</ul>
<p>With this data, you can identify which resources are failing most frequently, prioritize fixes, and implement fallback strategies for critical resources.</p>
<h3 id="heading-javascript-errors"><strong>JavaScript Errors</strong></h3>
<p>JavaScript errors occur during script execution and can prevent features from working properly. These include syntax errors, reference errors, type errors, and other runtime exceptions.</p>
<p>Using <code>window.onerror</code> can monitor JavaScript errors.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Monitor JavaScript errors</span>
<span class="hljs-built_in">window</span>.onerror = <span class="hljs-function">(<span class="hljs-params">msg, url, line, column, error</span>) =&gt;</span> {
    lazyReportCache({
        msg,
        line,
        column,
        <span class="hljs-attr">error</span>: error.stack,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'js'</span>,
        <span class="hljs-attr">pageURL</span>: url,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
        <span class="hljs-attr">startTime</span>: performance.now(),
    })
}
</code></pre>
<p>This handler captures detailed information about JavaScript errors:</p>
<ul>
<li><p>The error message</p>
</li>
<li><p>The file URL where the error occurred</p>
</li>
<li><p>The line and column number of the error</p>
</li>
<li><p>The full error stack trace</p>
</li>
</ul>
<p>This information is invaluable for debugging and fixing issues, especially in production environments where direct debugging isn't possible. By analyzing these errors, you can identify patterns and prioritize fixes for the most common or impactful issues.</p>
<h3 id="heading-promise-errors"><strong>Promise Errors</strong></h3>
<p>Modern JavaScript applications heavily use Promises for asynchronous operations. When a Promise rejection isn't handled with <code>.catch()</code> or a second argument to <code>.then()</code>, it results in an unhandled rejection which can cause silent failures.</p>
<p>Using <code>addEventListener()</code> to monitor the unhandledrejection event can capture unhandled promise errors.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Monitor promise errors - drawback is can't get column data</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'unhandledrejection'</span>, <span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
    lazyReportCache({
        <span class="hljs-attr">reason</span>: e.reason?.stack,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'promise'</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
        <span class="hljs-attr">startTime</span>: e.timeStamp,
        <span class="hljs-attr">pageURL</span>: getPageURL(),
    })
})
</code></pre>
<p>This code captures unhandled Promise rejections and reports:</p>
<ul>
<li><p>The rejection reason (usually an error object with a stack trace)</p>
</li>
<li><p>The timestamp when the rejection occurred</p>
</li>
<li><p>The page URL where the rejection happened</p>
</li>
</ul>
<p>Tracking unhandled Promise rejections is particularly important for asynchronous operations like API calls, where errors might otherwise go unnoticed. By monitoring these rejections, you can ensure that all asynchronous errors are properly handled and resolved.</p>
<h3 id="heading-sourcemap"><strong>Sourcemap</strong></h3>
<p>Generally, production environment code is minified, and sourcemap files are not uploaded to production. Therefore, error messages in production environment code are difficult to read. For this reason, we can use <a target="_blank" href="https://github.com/mozilla/source-map">source-map</a> to restore these minified code error messages.</p>
<p>When code errors occur, we can get the corresponding filename, line number, and column number:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-attr">line</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">column</span>: <span class="hljs-number">17</span>,
    <span class="hljs-attr">file</span>: <span class="hljs-string">'https:/www.xxx.com/bundlejs'</span>,
}
</code></pre>
<p>Then call the following code to restore:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parse</span>(<span class="hljs-params">error</span>) </span>{
    <span class="hljs-keyword">const</span> mapObj = <span class="hljs-built_in">JSON</span>.parse(getMapFileContent(error.url))
    <span class="hljs-keyword">const</span> consumer = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> sourceMap.SourceMapConsumer(mapObj)
    <span class="hljs-comment">// Remove ./ from webpack://source-map-demo/./src/index.js file</span>
    <span class="hljs-keyword">const</span> sources = mapObj.sources.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> format(item))
    <span class="hljs-comment">// Get original line and column numbers and source file based on minified error information</span>
    <span class="hljs-keyword">const</span> originalInfo = consumer.originalPositionFor({ <span class="hljs-attr">line</span>: error.line, <span class="hljs-attr">column</span>: error.column })
    <span class="hljs-comment">// sourcesContent contains the original source code of each file before minification, find corresponding source code by filename</span>
    <span class="hljs-keyword">const</span> originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">file</span>: originalInfo.source,
        <span class="hljs-attr">content</span>: originalFileContent,
        <span class="hljs-attr">line</span>: originalInfo.line,
        <span class="hljs-attr">column</span>: originalInfo.column,
        <span class="hljs-attr">msg</span>: error.msg,
        <span class="hljs-attr">error</span>: error.error
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">format</span>(<span class="hljs-params">item</span>) </span>{
    <span class="hljs-keyword">return</span> item.replace(<span class="hljs-regexp">/(\.\/)*/g</span>, <span class="hljs-string">''</span>)
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMapFileContent</span>(<span class="hljs-params">url</span>) </span>{
    <span class="hljs-keyword">return</span> fs.readFileSync(path.resolve(__dirname, <span class="hljs-string">`./maps/<span class="hljs-subst">${url.split(<span class="hljs-string">'/'</span>).pop()}</span>.map`</span>), <span class="hljs-string">'utf-8'</span>)
}
</code></pre>
<p>Each time the project is built, if sourcemap is enabled, each JS file will have a corresponding map file.</p>
<pre><code class="lang-javascript">bundle.js
bundle.js.map
</code></pre>
<p>At this point, the JS file is placed on the static server for user access, while the map file is stored on the server for error message restoration. The <code>source-map</code> library can restore error messages from minified code to their original state. For example, if the minified code error location is <code>line 1, column 47</code>, the restored location might be <code>line 4, column 10</code>. Besides location information, we can also get the original source code.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/be3d758960a8e8e494066820ef5708d8d06bda3df1a380bab37e007a91d16003/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f62316336623565656262376234656635396434646436616436313334383465627e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f"><img src="https://camo.githubusercontent.com/be3d758960a8e8e494066820ef5708d8d06bda3df1a380bab37e007a91d16003/68747470733a2f2f70332d6a75656a696e2e62797465696d672e636f6d2f746f732d636e2d692d6b3375316662706663702f62316336623565656262376234656635396434646436616436313334383465627e74706c762d6b3375316662706663702d77617465726d61726b2e696d6167653f" alt="Sourcemap error restoration example showing minified vs original code" width="600" height="400" loading="lazy"></a></p>
<p>The image above shows an example of code error restoration. Since this part doesn't belong to the SDK's scope, I created another <a target="_blank" href="https://github.com/woai3c/source-map-demo">repository</a> to handle this. Feel free to check it out if you're interested.</p>
<h3 id="heading-vue-errors"><strong>Vue Errors</strong></h3>
<p>Using <code>window.onerror</code> cannot capture Vue errors – we need to use Vue's provided API for monitoring.</p>
<pre><code class="lang-javascript">Vue.config.errorHandler = <span class="hljs-function">(<span class="hljs-params">err, vm, info</span>) =&gt;</span> {
    <span class="hljs-comment">// Print error information to console</span>
    <span class="hljs-built_in">console</span>.error(err)

    lazyReportCache({
        info,
        <span class="hljs-attr">error</span>: err.stack,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'vue'</span>,
        <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>,
        <span class="hljs-attr">startTime</span>: performance.now(),
        <span class="hljs-attr">pageURL</span>: getPageURL(),
    })
}
</code></pre>
<h2 id="heading-behavior-data-collection"><strong>Behavior Data Collection</strong></h2>
<p>Understanding how users interact with your application is crucial for optimizing user experience, improving engagement, and driving business goals. Behavior monitoring tracks user actions, navigation patterns, and engagement metrics to provide insights into how your application is actually being used.</p>
<p>In this section, we'll explore how to collect key behavioral metrics that can help you make data-driven decisions to improve your application.</p>
<h3 id="heading-pv-and-uv"><strong>PV and UV</strong></h3>
<p>PV (Page View) is the number of page views, while UV (Unique Visitor) is the number of unique users visiting. PV counts each page visit, while UV only counts once per user per day.</p>
<p><strong>Why this matters</strong>: PV and UV metrics help you understand your application's traffic patterns. A high PV-to-UV ratio indicates users are viewing multiple pages, suggesting good engagement. Tracking these metrics over time helps you identify growth trends, seasonal patterns, and the impact of marketing campaigns or feature releases.</p>
<p>For the front end, we just need to report PV each time a page is entered. UV statistics are handled on the server side, mainly analyzing reported data to calculate UV.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pv</span>(<span class="hljs-params"></span>) </span>{
    lazyReportCache({
        <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
        <span class="hljs-attr">subType</span>: <span class="hljs-string">'pv'</span>,
        <span class="hljs-attr">startTime</span>: performance.now(),
        <span class="hljs-attr">pageURL</span>: getPageURL(),
        <span class="hljs-attr">referrer</span>: <span class="hljs-built_in">document</span>.referrer,
        <span class="hljs-attr">uuid</span>: getUUID(),
    })
}
</code></pre>
<p>You can use this data to:</p>
<ul>
<li><p>Track which pages are most popular</p>
</li>
<li><p>Identify underperforming pages that need improvement</p>
</li>
<li><p>Analyze user flow through your application</p>
</li>
<li><p>Measure the effectiveness of new features or content</p>
</li>
</ul>
<h3 id="heading-page-stay-duration"><strong>Page Stay Duration</strong></h3>
<p>To get the stay duration, you just need to record an initial time when users enter the page, then subtract the initial time from the current time when users leave the page. This calculation logic can be placed in the <code>beforeunload</code> event.</p>
<p><strong>Why this matters</strong>: Page stay duration indicates how engaging your content is. Longer durations typically suggest users find the content valuable, while very short durations might indicate confusion, irrelevant content, or usability issues. This metric helps you identify which pages effectively capture user attention and which ones need improvement.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pageAccessDuration</span>(<span class="hljs-params"></span>) </span>{
    onBeforeunload(<span class="hljs-function">() =&gt;</span> {
        report({
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'page-access-duration'</span>,
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">pageURL</span>: getPageURL(),
            <span class="hljs-attr">uuid</span>: getUUID(),
        }, <span class="hljs-literal">true</span>)
    })
}
</code></pre>
<p>With page stay duration data, you can:</p>
<ul>
<li><p>Identify engaging vs. problematic content</p>
</li>
<li><p>Set benchmarks for content performance</p>
</li>
<li><p>Detect potential usability issues (extremely short durations)</p>
</li>
<li><p>Measure the effectiveness of content updates or redesigns</p>
</li>
</ul>
<h3 id="heading-page-access-depth"><strong>Page Access Depth</strong></h3>
<p>Recording page access depth is very useful. For example, for different activity pages a and b, if page a has an average access depth of 50% and page b has 80%, it indicates that page b is more popular with users. Based on this, we can make targeted improvements to page a.</p>
<p><strong>Why this matters</strong>: Access depth measures how far users scroll down a page, revealing whether they're viewing all your content or abandoning it partway through. This metric helps identify content engagement patterns and potential issues with content structure or page length.</p>
<p>Also, we can use access depth and stay duration to identify e-commerce order fraud. For example, if someone enters the page and immediately scrolls to the bottom, then waits a while before purchasing, while another person slowly scrolls down the page before purchasing. Even though they have the same stay duration, the first person is more likely to be committing fraud.</p>
<p>The page access depth calculation process is slightly more complex:</p>
<ol>
<li><p>When users enter the page, record the current time, scrollTop value, viewport height, and total page height.</p>
</li>
<li><p>When users scroll the page, the <code>scroll</code> event triggers. In the callback function, use the data from point 1 to calculate page access depth and stay duration.</p>
</li>
<li><p>When users stop scrolling at a certain point to continue viewing the page, record the current time, scrollTop value, viewport height, and total page height.</p>
</li>
<li><p>Repeat point 2...</p>
</li>
</ol>
<p>Here's the specific code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> timer
<span class="hljs-keyword">let</span> startTime = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> hasReport = <span class="hljs-literal">false</span>
<span class="hljs-keyword">let</span> pageHeight = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> scrollTop = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> viewportHeight = <span class="hljs-number">0</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pageAccessHeight</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'scroll'</span>, onScroll)

    onBeforeunload(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> now = performance.now()
        report({
            <span class="hljs-attr">startTime</span>: now,
            <span class="hljs-attr">duration</span>: now - startTime,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'page-access-height'</span>,
            <span class="hljs-attr">pageURL</span>: getPageURL(),
            <span class="hljs-attr">value</span>: toPercent((scrollTop + viewportHeight) / pageHeight),
            <span class="hljs-attr">uuid</span>: getUUID(),
        }, <span class="hljs-literal">true</span>)
    })

    <span class="hljs-comment">// Initialize and record current access height and time after page loads</span>
    executeAfterLoad(<span class="hljs-function">() =&gt;</span> {
        startTime = performance.now()
        pageHeight = <span class="hljs-built_in">document</span>.documentElement.scrollHeight || <span class="hljs-built_in">document</span>.body.scrollHeight
        scrollTop = <span class="hljs-built_in">document</span>.documentElement.scrollTop || <span class="hljs-built_in">document</span>.body.scrollTop
        viewportHeight = <span class="hljs-built_in">window</span>.innerHeight
    })
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onScroll</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">clearTimeout</span>(timer)
    <span class="hljs-keyword">const</span> now = performance.now()

    <span class="hljs-keyword">if</span> (!hasReport) {
        hasReport = <span class="hljs-literal">true</span>
        lazyReportCache({
            <span class="hljs-attr">startTime</span>: now,
            <span class="hljs-attr">duration</span>: now - startTime,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'page-access-height'</span>,
            <span class="hljs-attr">pageURL</span>: getPageURL(),
            <span class="hljs-attr">value</span>: toPercent((scrollTop + viewportHeight) / pageHeight),
            <span class="hljs-attr">uuid</span>: getUUID(),
        })
    }

    timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        hasReport = <span class="hljs-literal">false</span>
        startTime = now
        pageHeight = <span class="hljs-built_in">document</span>.documentElement.scrollHeight || <span class="hljs-built_in">document</span>.body.scrollHeight
        scrollTop = <span class="hljs-built_in">document</span>.documentElement.scrollTop || <span class="hljs-built_in">document</span>.body.scrollTop
        viewportHeight = <span class="hljs-built_in">window</span>.innerHeight        
    }, <span class="hljs-number">500</span>)
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toPercent</span>(<span class="hljs-params">val</span>) </span>{
    <span class="hljs-keyword">if</span> (val &gt;= <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> <span class="hljs-string">'100%'</span>
    <span class="hljs-keyword">return</span> (val * <span class="hljs-number">100</span>).toFixed(<span class="hljs-number">2</span>) + <span class="hljs-string">'%'</span>
}
</code></pre>
<p>With page access depth data, you can:</p>
<ul>
<li><p>Identify where users lose interest in your content</p>
</li>
<li><p>Optimize content placement (put important elements where users actually look)</p>
</li>
<li><p>Improve long-form content structure with better hierarchy</p>
</li>
<li><p>Detect unusual user behavior patterns that might indicate fraud or bots</p>
</li>
</ul>
<h3 id="heading-user-clicks"><strong>User Clicks</strong></h3>
<p>Using <code>addEventListener()</code> to monitor <code>mousedown</code> and <code>touchstart</code> events, we can collect information about each click area's size, click coordinates' specific position in the page, clicked element's content, and other information.</p>
<p><strong>Why this matters</strong>: Click tracking reveals what elements users interact with most frequently, helping you understand user interests and optimize UI element placement. It also helps identify usability issues where users might be clicking on non-interactive elements expecting a response.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onClick</span>(<span class="hljs-params"></span>) </span>{
    [<span class="hljs-string">'mousedown'</span>, <span class="hljs-string">'touchstart'</span>].forEach(<span class="hljs-function"><span class="hljs-params">eventType</span> =&gt;</span> {
        <span class="hljs-keyword">let</span> timer
        <span class="hljs-built_in">window</span>.addEventListener(eventType, <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
            <span class="hljs-built_in">clearTimeout</span>(timer)
            timer = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
                <span class="hljs-keyword">const</span> target = event.target
                <span class="hljs-keyword">const</span> { top, left } = target.getBoundingClientRect()

                lazyReportCache({
                    top,
                    left,
                    eventType,
                    <span class="hljs-attr">pageHeight</span>: <span class="hljs-built_in">document</span>.documentElement.scrollHeight || <span class="hljs-built_in">document</span>.body.scrollHeight,
                    <span class="hljs-attr">scrollTop</span>: <span class="hljs-built_in">document</span>.documentElement.scrollTop || <span class="hljs-built_in">document</span>.body.scrollTop,
                    <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
                    <span class="hljs-attr">subType</span>: <span class="hljs-string">'click'</span>,
                    <span class="hljs-attr">target</span>: target.tagName,
                    <span class="hljs-attr">paths</span>: event.path?.map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.tagName).filter(<span class="hljs-built_in">Boolean</span>),
                    <span class="hljs-attr">startTime</span>: event.timeStamp,
                    <span class="hljs-attr">pageURL</span>: getPageURL(),
                    <span class="hljs-attr">outerHTML</span>: target.outerHTML,
                    <span class="hljs-attr">innerHTML</span>: target.innerHTML,
                    <span class="hljs-attr">width</span>: target.offsetWidth,
                    <span class="hljs-attr">height</span>: target.offsetHeight,
                    <span class="hljs-attr">viewport</span>: {
                        <span class="hljs-attr">width</span>: <span class="hljs-built_in">window</span>.innerWidth,
                        <span class="hljs-attr">height</span>: <span class="hljs-built_in">window</span>.innerHeight,
                    },
                    <span class="hljs-attr">uuid</span>: getUUID(),
                })
            }, <span class="hljs-number">500</span>)
        })
    })
}
</code></pre>
<p>With this click data, you can:</p>
<ul>
<li><p>Create heatmaps showing where users click most frequently</p>
</li>
<li><p>Identify non-functional elements users try to interact with</p>
</li>
<li><p>Optimize button placement and size for better conversion</p>
</li>
<li><p>Detect rage clicks (multiple rapid clicks in the same area) indicating user frustration</p>
</li>
</ul>
<h3 id="heading-page-navigation"><strong>Page Navigation</strong></h3>
<p>Using <code>addEventListener()</code> to monitor <code>popstate</code> and <code>hashchange</code> page navigation events allows you to track how users navigate through your application.</p>
<p><strong>Why this matters</strong>: Navigation tracking helps you understand user flow patterns - how users move between pages, which navigation paths are most common, and where users might be getting lost or trapped in navigation loops. This data is crucial for optimizing site structure and improving user journey flows.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">pageChange</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">from</span> = <span class="hljs-string">''</span>
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'popstate'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> to = getPageURL()

        lazyReportCache({
            <span class="hljs-keyword">from</span>,
            to,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'popstate'</span>,
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">uuid</span>: getUUID(),
        })

        <span class="hljs-keyword">from</span> = to
    }, <span class="hljs-literal">true</span>)

    <span class="hljs-keyword">let</span> oldURL = <span class="hljs-string">''</span>
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'hashchange'</span>, <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
        <span class="hljs-keyword">const</span> newURL = event.newURL

        lazyReportCache({
            <span class="hljs-attr">from</span>: oldURL,
            <span class="hljs-attr">to</span>: newURL,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: <span class="hljs-string">'hashchange'</span>,
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">uuid</span>: getUUID(),
        })

        oldURL = newURL
    }, <span class="hljs-literal">true</span>)
}
</code></pre>
<p>With navigation data, you can:</p>
<ul>
<li><p>Identify common user paths through your application</p>
</li>
<li><p>Detect navigation dead-ends or loops where users get stuck</p>
</li>
<li><p>Optimize navigation menus based on actual usage patterns</p>
</li>
<li><p>Improve information architecture to better match user behavior</p>
</li>
</ul>
<h3 id="heading-vue-router-changes"><strong>Vue Router Changes</strong></h3>
<p>For applications built with Vue, you can use the router's hooks to monitor navigation between routes, providing similar insights to general page navigation tracking but specific to Vue's routing system.</p>
<p><strong>Why this matters</strong>: In single-page applications, traditional page navigation events don't capture all route changes. Framework-specific router monitoring ensures you don't miss important navigation data in modern web applications.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onVueRouter</span>(<span class="hljs-params">router</span>) </span>{
    router.beforeEach(<span class="hljs-function">(<span class="hljs-params">to, <span class="hljs-keyword">from</span>, next</span>) =&gt;</span> {
        <span class="hljs-comment">// Don't count first page load</span>
        <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">from</span>.name) {
            <span class="hljs-keyword">return</span> next()
        }

        <span class="hljs-keyword">const</span> data = {
            <span class="hljs-attr">params</span>: to.params,
            <span class="hljs-attr">query</span>: to.query,
        }

        lazyReportCache({
            data,
            <span class="hljs-attr">name</span>: to.name || to.path,
            <span class="hljs-attr">type</span>: <span class="hljs-string">'behavior'</span>,
            <span class="hljs-attr">subType</span>: [<span class="hljs-string">'vue-router-change'</span>, <span class="hljs-string">'pv'</span>],
            <span class="hljs-attr">startTime</span>: performance.now(),
            <span class="hljs-attr">from</span>: <span class="hljs-keyword">from</span>.fullPath,
            <span class="hljs-attr">to</span>: to.fullPath,
            <span class="hljs-attr">uuid</span>: getUUID(),
        })

        next()
    })
}
</code></pre>
<p>This data helps you:</p>
<ul>
<li><p>Track the most frequently accessed routes in your Vue application</p>
</li>
<li><p>Understand navigation patterns specific to your application's structure</p>
</li>
<li><p>Identify potential optimization opportunities in your routing setup</p>
</li>
<li><p>Measure the impact of UX improvements on navigation behavior</p>
</li>
</ul>
<h2 id="heading-data-reporting"><strong>Data Reporting</strong></h2>
<p>Once you've collected performance, error, and behavior data, you need a reliable system to transmit this information to your backend for processing and analysis. Data reporting is the critical bridge between client-side data collection and server-side analytics.</p>
<p>Effective data reporting must balance several concerns:</p>
<ol>
<li><p><strong>Reliability</strong> – Ensuring data is successfully transmitted, especially critical errors</p>
</li>
<li><p><strong>Performance</strong> – Minimizing impact on the user experience and application performance</p>
</li>
<li><p><strong>Timing</strong> – Deciding when to send data to avoid interference with user interactions</p>
</li>
<li><p><strong>Bandwidth</strong> – Managing the amount of data transmitted to reduce network usage</p>
</li>
</ol>
<p>Let's explore the various methods and strategies for implementing efficient data reporting.</p>
<h3 id="heading-reporting-methods"><strong>Reporting Methods</strong></h3>
<p>Data can be reported using the following methods:</p>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></p>
</li>
<li><p>image</p>
</li>
</ul>
<p>My simple SDK uses a combination of the first and second methods for reporting. Using sendBeacon for reporting has very obvious advantages.</p>
<p><strong>Note</strong>: Using the <code>sendBeacon()</code> method will send data to the server asynchronously when the user agent has an opportunity, without delaying page unload or affecting the performance of the next navigation. This solves all the problems with submitting analytics data: data is reliable, transmission is asynchronous, and it won't affect the loading of the next page.</p>
<p>For browsers that don't support sendBeacon, we can use XMLHttpRequest for reporting. An HTTP request consists of sending and receiving two steps.</p>
<p>Actually, for reporting, we just need to ensure the data can be sent – we don't need to receive the response. For this reason, I did an experiment where I sent 30kb of data (generally reported data rarely exceeds this size) using XMLHttpRequest in beforeunload, tested with different browsers, and all were able to send successfully. Of course, this also depends on hardware performance and network conditions.</p>
<p>Here's a sample implementation of a reporting function that uses both methods:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">report</span>(<span class="hljs-params">data, isImmediate = false</span>) </span>{
    <span class="hljs-keyword">if</span> (!config.reportUrl) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Report URL is not set'</span>)
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-comment">// Add timestamp and other common properties</span>
    <span class="hljs-keyword">const</span> reportData = {
        ...data,
        <span class="hljs-attr">timestamp</span>: <span class="hljs-built_in">Date</span>.now(),
        <span class="hljs-attr">userAgent</span>: navigator.userAgent,
        <span class="hljs-attr">userId</span>: getUserId(),
        <span class="hljs-comment">// Add other common properties as needed</span>
    }

    <span class="hljs-comment">// Choose reporting method based on browser support and timing</span>
    <span class="hljs-keyword">if</span> (isImmediate) {
        sendData(reportData)
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Queue data for batch sending</span>
        reportQueue.push(reportData)

        <span class="hljs-comment">// Send when queue reaches threshold</span>
        <span class="hljs-keyword">if</span> (reportQueue.length &gt;= config.batchSize) {
            sendBatchData()
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendData</span>(<span class="hljs-params">data</span>) </span>{
    <span class="hljs-comment">// Try sendBeacon first</span>
    <span class="hljs-keyword">if</span> (navigator.sendBeacon) {
        <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">new</span> Blob([<span class="hljs-built_in">JSON</span>.stringify(data)], { <span class="hljs-attr">type</span>: <span class="hljs-string">'application/json'</span> })
        <span class="hljs-keyword">const</span> success = navigator.sendBeacon(config.reportUrl, blob)

        <span class="hljs-keyword">if</span> (success) <span class="hljs-keyword">return</span>
    }

    <span class="hljs-comment">// Fall back to XMLHttpRequest</span>
    <span class="hljs-keyword">const</span> xhr = <span class="hljs-keyword">new</span> XMLHttpRequest()
    xhr.open(<span class="hljs-string">'POST'</span>, config.reportUrl, <span class="hljs-literal">true</span>)
    xhr.setRequestHeader(<span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'application/json'</span>)
    xhr.send(<span class="hljs-built_in">JSON</span>.stringify(data))
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendBatchData</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (reportQueue.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>

    <span class="hljs-keyword">const</span> data = [...reportQueue]
    reportQueue.length = <span class="hljs-number">0</span>

    sendData({ <span class="hljs-attr">type</span>: <span class="hljs-string">'batch'</span>, data })
}
</code></pre>
<h3 id="heading-reporting-timing"><strong>Reporting Timing</strong></h3>
<p>There are three reporting timings:</p>
<ol>
<li><p>Use <code>requestIdleCallback/setTimeout</code> for delayed reporting</p>
</li>
<li><p>Report in the beforeunload callback function</p>
</li>
<li><p>Cache reported data and report when reaching a certain amount</p>
</li>
</ol>
<p>It's recommended to combine all three methods:</p>
<ol>
<li><p>First cache the reported data, and when reaching a certain amount, use <code>requestIdleCallback/setTimeout</code> for delayed reporting</p>
</li>
<li><p>Report all unreported data when leaving the page</p>
</li>
</ol>
<p>Here's how you might implement this combined approach:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Cache for storing reports until they're sent</span>
<span class="hljs-keyword">let</span> reportCache = []
<span class="hljs-keyword">const</span> MAX_CACHE_SIZE = <span class="hljs-number">10</span>
<span class="hljs-keyword">let</span> timer = <span class="hljs-literal">null</span>

<span class="hljs-comment">// Report data with requestIdleCallback when browser is idle</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">lazyReportCache</span>(<span class="hljs-params">data</span>) </span>{
    reportCache.push(data)

    <span class="hljs-comment">// If cache reaches threshold, schedule sending</span>
    <span class="hljs-keyword">if</span> (reportCache.length &gt;= MAX_CACHE_SIZE) {
        <span class="hljs-comment">// Use requestIdleCallback if available, otherwise setTimeout</span>
        <span class="hljs-keyword">const</span> scheduleFn = <span class="hljs-built_in">window</span>.requestIdleCallback || <span class="hljs-built_in">setTimeout</span>

        <span class="hljs-keyword">if</span> (timer) {
            cancelScheduledReport()
        }

        timer = scheduleFn(<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-comment">// Send cached data in bulk</span>
            <span class="hljs-keyword">const</span> dataToSend = [...reportCache]
            reportCache = []
            report({
                <span class="hljs-attr">type</span>: <span class="hljs-string">'batch'</span>,
                <span class="hljs-attr">data</span>: dataToSend,
            })
            timer = <span class="hljs-literal">null</span>
        }, { <span class="hljs-attr">timeout</span>: <span class="hljs-number">2000</span> }) <span class="hljs-comment">// For requestIdleCallback, timeout after 2s</span>
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cancelScheduledReport</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.requestIdleCallback &amp;&amp; timer) {
        <span class="hljs-built_in">window</span>.cancelIdleCallback(timer)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (timer) {
        <span class="hljs-built_in">clearTimeout</span>(timer)
    }
    timer = <span class="hljs-literal">null</span>
}

<span class="hljs-comment">// Report any remaining data when user leaves the page</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setupUnloadReporting</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'beforeunload'</span>, <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (reportCache.length &gt; <span class="hljs-number">0</span>) {
            <span class="hljs-comment">// Cancel any scheduled reporting</span>
            cancelScheduledReport()

            <span class="hljs-comment">// Send remaining cached data immediately</span>
            report({
                <span class="hljs-attr">type</span>: <span class="hljs-string">'batch'</span>,
                <span class="hljs-attr">data</span>: reportCache,
            }, <span class="hljs-literal">true</span>) <span class="hljs-comment">// true for immediate sending</span>

            reportCache = []
        }
    })
}
</code></pre>
<p>This implementation:</p>
<ol>
<li><p>Collects data in a cache until it reaches a threshold</p>
</li>
<li><p>Uses <code>requestIdleCallback</code> (or <code>setTimeout</code> as fallback) to send data when the browser is idle</p>
</li>
<li><p>Ensures any remaining data is sent when the user leaves the page</p>
</li>
<li><p>Batches multiple reports together to reduce network requests</p>
</li>
</ol>
<p>By combining these methods, you create a robust reporting system that minimizes performance impact while ensuring data reliability.</p>
<h2 id="heading-summary"><strong>Summary</strong></h2>
<p>In this comprehensive guide, we've explored how to build a complete frontend monitoring SDK for collecting and reporting critical application data. Let's recap what we've covered:</p>
<ol>
<li><p><strong>Performance Monitoring</strong></p>
<ul>
<li><p>We implemented methods to capture key web vitals like FP, FCP, LCP, and CLS</p>
</li>
<li><p>We tracked page load events, API request timing, and resource loading metrics</p>
</li>
<li><p>We measured first screen rendering time and frame rates to ensure smooth user experiences</p>
</li>
<li><p>We added support for SPA-specific metrics like Vue router change rendering time</p>
</li>
</ul>
</li>
<li><p><strong>Error Monitoring</strong></p>
<ul>
<li><p>We built systems to capture resource loading errors, JavaScript exceptions, and Promise rejections</p>
</li>
<li><p>We explored how to use sourcemaps to make minified production errors readable</p>
</li>
<li><p>We integrated with framework-specific error handling for Vue applications</p>
</li>
</ul>
</li>
<li><p><strong>User Behavior Tracking</strong></p>
<ul>
<li><p>We implemented tracking for page views, stay duration, and scroll depth</p>
</li>
<li><p>We created methods to monitor user clicks and navigation patterns</p>
</li>
<li><p>We built custom tracking for SPA navigation with Vue Router</p>
</li>
</ul>
</li>
<li><p><strong>Data Reporting</strong></p>
<ul>
<li><p>We developed robust reporting mechanisms using sendBeacon and XMLHttpRequest</p>
</li>
<li><p>We implemented intelligent reporting timing strategies to minimize performance impact</p>
</li>
<li><p>We created batching mechanisms to reduce network requests</p>
</li>
</ul>
</li>
</ol>
<p>Building your own monitoring SDK gives you complete control over what data you collect and how you process it. This approach offers several advantages over third-party solutions:</p>
<ul>
<li><p><strong>Privacy</strong>: You own all the data and can ensure compliance with regulations like GDPR</p>
</li>
<li><p><strong>Performance</strong>: You can optimize the SDK specifically for your application's needs</p>
</li>
<li><p><strong>Customization</strong>: You can add custom metrics unique to your business requirements</p>
</li>
<li><p><strong>Integration</strong>: Your SDK can easily integrate with your existing systems</p>
</li>
</ul>
<p>As you implement your own monitoring solution, remember these best practices:</p>
<ol>
<li><p><strong>Respect User Privacy</strong>: Only collect what you need and be transparent about it</p>
</li>
<li><p><strong>Minimize Performance Impact</strong>: Ensure your monitoring doesn't degrade the user experience</p>
</li>
<li><p><strong>Balance Detail and Volume</strong>: More data isn't always better if it overwhelms your analysis</p>
</li>
<li><p><strong>Act on Insights</strong>: The ultimate goal is to improve your application based on the data</p>
</li>
</ol>
<p>By following the approaches outlined in this article, you'll be well-equipped to build a comprehensive monitoring system that helps you deliver better user experiences through data-driven decision making.</p>
<h2 id="heading-references"><strong>References</strong></h2>
<h3 id="heading-performance-monitoring"><strong>Performance Monitoring</strong></h3>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API">Performance API</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming">PerformanceResourceTiming</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API">Using_the_Resource_Timing_API</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming">PerformanceTiming</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/metrics/">Metrics</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/evolving-cls/">evolving-cls</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/custom-metrics/">custom-metrics</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/GoogleChrome/web-vitals">web-vitals</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver">PerformanceObserver</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Element_timing_API">Element_timing_API</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming">PerformanceEventTiming</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin">Timing-Allow-Origin</a></p>
</li>
<li><p><a target="_blank" href="https://web.dev/bfcache/">bfcache</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></p>
</li>
</ul>
<h3 id="heading-error-monitoring"><strong>Error Monitoring</strong></h3>
<ul>
<li><p><a target="_blank" href="https://github.com/joeyguo/noerror">noerror</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/mozilla/source-map">source-map</a></p>
</li>
</ul>
<h3 id="heading-behavior-monitoring"><strong>Behavior Monitoring</strong></h3>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event">popstate</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event">hashchange</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Front-End Performance Optimization Handbook – Tips and Strategies for Devs ]]>
                </title>
                <description>
                    <![CDATA[ When you’re building a website, you’ll want it to be responsive, fast, and efficient. This means making sure the site loads quickly, runs smoothly, and provides a seamless experience for your users, among other things. So as you build, you’ll want to... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-front-end-performance-optimization-handbook/</link>
                <guid isPermaLink="false">681b5e61b1ed0b90facd0adf</guid>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ frontend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gordan Tan ]]>
                </dc:creator>
                <pubDate>Wed, 07 May 2025 13:21:37 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746468304666/ca24ac6b-1591-4abf-a544-739fbfaecf49.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you’re building a website, you’ll want it to be responsive, fast, and efficient. This means making sure the site loads quickly, runs smoothly, and provides a seamless experience for your users, among other things.</p>
<p>So as you build, you’ll want to keep various performance optimizations in mind – like reducing file size, making fewer server requests, optimizing images in various ways, and so on.</p>
<p>But performance optimization is a double-edged sword, with both good and bad aspects. The good side is that it can improve website performance, while the bad side is that it's complicated to configure, and there are many rules to follow.</p>
<p>Also, some performance optimization rules aren't suitable for all scenarios and should be used with caution. So make sure you approach this handbook with a critical eye. In it, I’ll lay out a bunch of ways you can optimize your website’s performance, and share insights to help you chose which of these techniques to use.</p>
<p>I’ll also provide the references for these optimization suggestions after each one and at the end of the article.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-reduce-http-requests">Reduce HTTP Requests</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-http2">Use HTTP2</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-server-side-rendering">Use Server-Side Rendering</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-a-cdn-for-static-resources">Use a CDN for Static Resources</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-place-css-in-the-head-and-javascript-files-at-the-bottom">Place CSS in the Head and JavaScript Files at the Bottom</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-font-icons-iconfont-instead-of-image-icons">Use Font Icons (iconfont) Instead of Image Icons</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-make-good-use-of-caching-avoid-reloading-the-same-resources">Make Good Use of Caching, Avoid Reloading the Same Resources</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-compress-files">Compress Files</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-image-optimization">Image Optimization</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-lazy-loading-images">Lazy Loading Images</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-responsive-images">Responsive Images</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-adjust-image-size">Adjust Image Size</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reduce-image-quality">Reduce Image Quality</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-css3-effects-instead-of-images-when-possible">Use CSS3 Effects Instead of Images When Possible</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-webp-format-images">Use webp Format Images</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-load-code-on-demand-through-webpack-extract-third-party-libraries-reduce-redundant-code-when-converting-es6-to-es5">Load Code on Demand Through Webpack, Extract Third-Party Libraries, Reduce Redundant Code When Converting ES6 to ES5</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reduce-reflows-and-repaints">Reduce Reflows and Repaints</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-event-delegation">Use Event Delegation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-pay-attention-to-program-locality">Pay Attention to Program Locality</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-if-else-vs-switch">if-else vs switch</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lookup-tables">Lookup Tables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-avoid-page-stuttering">Avoid Page Stuttering</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-requestanimationframe-to-implement-visual-changes">Use requestAnimationFrame to Implement Visual Changes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-web-workers">Use Web Workers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-bitwise-operations">Use Bitwise Operations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-dont-override-native-methods">Don't Override Native Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reduce-the-complexity-of-css-selectors">Reduce the Complexity of CSS Selectors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-flexbox-instead-of-earlier-layout-models">Use Flexbox Instead of Earlier Layout Models</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-transform-and-opacity-properties-to-implement-animations">Use Transform and Opacity Properties to Implement Animations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-rules-reasonably-avoid-over-optimization">Use Rules Reasonably, Avoid Over-Optimization</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-other-references">Other References</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-1-reduce-http-requests"><strong>1. Reduce HTTP Requests</strong></h2>
<p>A complete HTTP request needs to go through DNS lookup, TCP handshake, browser sending the HTTP request, server receiving the request, server processing the request and sending back a response, browser receiving the response, and other processes. Let's look at a specific example to understand how HTTP works:</p>
<p><img src="https://camo.githubusercontent.com/7988c06bb7b698dcc66ac8f2556cbe03b239ba2c8bf17ecddb29004c74b0eb36/68747470733a2f2f692d626c6f672e6373646e696d672e636e2f626c6f675f6d6967726174652f64333736643731343630633763376331316462316338353134366230343164632e706e67" alt="HTTP request waterfall showing timing breakdown" width="600" height="400" loading="lazy"></p>
<p>This is an HTTP request, and the file size is 28.4KB.</p>
<p>Terminology explained:</p>
<ul>
<li><p>Queueing: Time spent in the request queue.</p>
</li>
<li><p>Stalled: The time difference between when the TCP connection is established and when data can actually be transmitted, including proxy negotiation time.</p>
</li>
<li><p>Proxy negotiation: Time spent negotiating with the proxy server.</p>
</li>
<li><p>DNS Lookup: Time spent performing DNS lookup. Each different domain on a page requires a DNS lookup.</p>
</li>
<li><p>Initial Connection / Connecting: Time spent establishing a connection, including TCP handshake/retry and SSL negotiation.</p>
</li>
<li><p>SSL: Time spent completing the SSL handshake.</p>
</li>
<li><p>Request sent: Time spent sending the network request, usually a millisecond.</p>
</li>
<li><p>Waiting (TFFB): TFFB is the time from when the page request is made until the first byte of response data is received.</p>
</li>
<li><p>Content Download: Time spent receiving the response data.</p>
</li>
</ul>
<p>From this example, we can see that the actual data download time accounts for only <code>13.05 / 204.16 = 6.39%</code> of the total. The smaller the file, the smaller this ratio – and the larger the file, the higher the ratio. This is why it's recommended to combine multiple small files into one large file, which reduces the number of HTTP requests.</p>
<h3 id="heading-how-to-combine-multiple-files"><strong>How to combine multiple files</strong></h3>
<p>There are several techniques to reduce the number of HTTP requests by combining files:</p>
<p><strong>1. Bundle JavaScript files with Webpack</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// webpack.config.js</span>
<span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = {
  entry: <span class="hljs-string">'./src/index.js'</span>,
  output: {
    filename: <span class="hljs-string">'bundle.js'</span>,
    path: path.resolve(__dirname, <span class="hljs-string">'dist'</span>),
  },
};
</code></pre>
<p>This will combine all JavaScript files imported in your entry point into a single bundle.</p>
<p><strong>2. Combine CSS files</strong><br>Using CSS preprocessors like Sass:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/* main.scss */</span>
<span class="hljs-meta">@import</span> <span class="hljs-string">'reset'</span>;
<span class="hljs-meta">@import</span> <span class="hljs-string">'variables'</span>;
<span class="hljs-meta">@import</span> <span class="hljs-string">'typography'</span>;
<span class="hljs-meta">@import</span> <span class="hljs-string">'layout'</span>;
<span class="hljs-meta">@import</span> <span class="hljs-string">'components'</span>;
</code></pre>
<p>Then compile to a single CSS file:</p>
<pre><code class="lang-typescript">sass main.scss:main.css
</code></pre>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Resource_timing">Resource_timing</a></li>
</ul>
<h2 id="heading-2-use-http2"><strong>2. Use HTTP2</strong></h2>
<p>Compared to HTTP1.1, HTTP2 has several advantages:</p>
<h3 id="heading-faster-parsing">Faster parsing</h3>
<p>When parsing HTTP1.1 requests, the server must continuously read bytes until it encounters the CRLF delimiter. Parsing HTTP2 requests isn't as complicated because HTTP2 is a frame-based protocol, and each frame has a field indicating its length.</p>
<h3 id="heading-multiplexing">Multiplexing</h3>
<p>With HTTP1.1, if you want to make multiple requests simultaneously, you need to establish multiple TCP connections because one TCP connection can only handle one HTTP1.1 request at a time.</p>
<p>In HTTP2, multiple requests can share a single TCP connection, which is called multiplexing. Each request and response is represented by a stream with a unique stream ID to identify it.<br>Multiple requests and responses can be sent out of order within the TCP connection and then reassembled at the destination using the stream ID.</p>
<h3 id="heading-header-compression">Header compression</h3>
<p>HTTP2 provides header compression functionality.</p>
<p>For example, consider the following two requests:</p>
<pre><code class="lang-typescript">:authority: unpkg.zhimg.com
:method: GET
:path: <span class="hljs-regexp">/za-js-sdk@2.16.0/</span>dist/zap.js
:scheme: https
accept: *<span class="hljs-comment">/*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36</span>
</code></pre>
<pre><code class="lang-typescript">:authority: zz.bdstatic.com
:method: GET
:path: <span class="hljs-regexp">/linksubmit/</span>push.js
:scheme: https
accept: *<span class="hljs-comment">/*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36</span>
</code></pre>
<p>From the two requests above, you can see that a lot of data is repeated. If we could store the same headers and only send the differences between them, we could save a lot of bandwidth and speed up the request time.</p>
<p>HTTP/2 uses "header tables" on the client and server sides to track and store previously sent key-value pairs, and for identical data, it's no longer sent through each request and response.</p>
<p>Here's a simplified example. Suppose the client sends the following header requests in sequence:</p>
<pre><code class="lang-typescript">Header1:foo
Header2:bar
Header3:bat
</code></pre>
<p>When the client sends a request, it creates a table based on the header values:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Index</td><td>Header Name</td><td>Value</td></tr>
</thead>
<tbody>
<tr>
<td>62</td><td>Header1</td><td>foo</td></tr>
<tr>
<td>63</td><td>Header2</td><td>bar</td></tr>
<tr>
<td>64</td><td>Header3</td><td>bat</td></tr>
</tbody>
</table>
</div><p>If the server receives the request, it will create the same table.<br>When the client sends the next request, if the headers are the same, it can directly send a header block like this:</p>
<pre><code class="lang-typescript"><span class="hljs-number">62</span> <span class="hljs-number">63</span> <span class="hljs-number">64</span>
</code></pre>
<p>The server will look up the previously established table and restore these numbers to the complete headers they correspond to.</p>
<h3 id="heading-priority">Priority</h3>
<p>HTTP2 can set a higher priority for more urgent requests, and the server can prioritize handling them after receiving such requests.</p>
<h3 id="heading-flow-control">Flow control</h3>
<p>Since the bandwidth of a TCP connection (depending on the network bandwidth from client to server) is fixed, when there are multiple concurrent requests, if one request occupies more traffic, another request will occupy less. Flow control can precisely control the flow of different streams.</p>
<h3 id="heading-server-push">Server push</h3>
<p>A powerful new feature added in HTTP2 is that the server can send multiple responses to a single client request. In other words, in addition to responding to the initial request, the server can also push additional resources to the client without the client explicitly requesting them.</p>
<p>For example, when a browser requests a website, in addition to returning the HTML page, the server can also proactively push resources based on the URLs of resources in the HTML page.</p>
<p>Many websites have already started using HTTP2, such as Zhihu:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/17c8f78f0341150240e6719ed82ee794e5c569404861581ccad306b88d9b6f6c/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f39636165316532313931613035393466393833373636646635636265373562352e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb0ovwimn9pg7z7eo0qxd.png" alt="show hot to check HTTP1 and HTTP2 protocols" width="600" height="400" loading="lazy"></a></p>
<p>Where "h2" refers to the HTTP2 protocol, and "http/1.1" refers to the HTTP1.1 protocol.</p>
<p>References:</p>
<ul>
<li><p><a target="_blank" href="https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn">HTTP2 Introduction</a></p>
</li>
<li><p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP">HTTP</a></p>
</li>
</ul>
<h2 id="heading-3-use-server-side-rendering"><strong>3. Use Server-Side Rendering</strong></h2>
<p>In client-side rendering, you get the HTML file, download JavaScript files as needed, run the files, generate the DOM, and then render.</p>
<p>And in server-side rendering, the server returns the HTML file, and the client only needs to parse the HTML.</p>
<ul>
<li><p>Pros: Faster first-screen rendering, better SEO.</p>
</li>
<li><p>Cons: Complicated configuration, increases the computational load on the server.</p>
</li>
</ul>
<p>Below, I'll use Vue SSR as an example to briefly describe the SSR process.</p>
<h3 id="heading-client-side-rendering-process">Client-side rendering process</h3>
<ol>
<li><p>Visit a client-rendered website.</p>
</li>
<li><p>The server returns an HTML file containing resource import statements and <code>&lt;div id="app"&gt;&lt;/div&gt;</code>.</p>
</li>
<li><p>The client requests resources from the server via HTTP, and when the necessary resources are loaded, it executes <code>new Vue()</code> to instantiate and render the page.</p>
</li>
</ol>
<p><strong>Example of client-side rendered app (Vue):</strong></p>
<pre><code class="lang-typescript">&lt;!-- index.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;Client-side Rendering Example&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- Initially empty container --&gt;
  &lt;div id=<span class="hljs-string">"app"</span>&gt;&lt;/div&gt;

  &lt;!-- JavaScript bundle that will render the content --&gt;
  &lt;script src=<span class="hljs-string">"/dist/bundle.js"</span>&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// main.js (compiled into bundle.js)</span>
<span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.vue'</span>;

<span class="hljs-comment">// Client-side rendering happens here - after JS loads and executes</span>
<span class="hljs-keyword">new</span> Vue({
  render: <span class="hljs-function"><span class="hljs-params">h</span> =&gt;</span> h(App)
}).$mount(<span class="hljs-string">'#app'</span>);
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// App.vue</span>
&lt;template&gt;
  &lt;div&gt;
    &lt;h1&gt;{{ title }}&lt;/h1&gt;
    &lt;p&gt;This content is rendered client-side.&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  data() {
    <span class="hljs-keyword">return</span> {
      title: <span class="hljs-string">'Hello World'</span>
    }
  },
  <span class="hljs-comment">// In client-side rendering, this lifecycle hook runs in the browser</span>
  mounted() {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Component mounted in browser'</span>);
  }
}
&lt;/script&gt;
</code></pre>
<h3 id="heading-server-side-rendering-process">Server-side rendering process</h3>
<ol>
<li><p>Visit a server-rendered website.</p>
</li>
<li><p>The server checks which resource files the current route component needs, then fills the content of these files into the HTML file. If there are AJAX requests, it will execute them for data pre-fetching and fill them into the HTML file, and finally return this HTML page.</p>
</li>
<li><p>When the client receives this HTML page, it can start rendering the page immediately. At the same time, the page also loads resources, and when the necessary resources are fully loaded, it begins to execute <code>new Vue()</code> to instantiate and take over the page.</p>
</li>
</ol>
<p><strong>Example of server-side rendered app (Vue):</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server.js</span>
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> server = express();
<span class="hljs-keyword">const</span> { createBundleRenderer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'vue-server-renderer'</span>);

<span class="hljs-comment">// Create a renderer based on the server bundle</span>
<span class="hljs-keyword">const</span> renderer = createBundleRenderer(<span class="hljs-string">'./dist/vue-ssr-server-bundle.json'</span>, {
  template: <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>).readFileSync(<span class="hljs-string">'./index.template.html'</span>, <span class="hljs-string">'utf-8'</span>),
  clientManifest: <span class="hljs-built_in">require</span>(<span class="hljs-string">'./dist/vue-ssr-client-manifest.json'</span>)
});

<span class="hljs-comment">// Handle all routes with the same renderer</span>
server.get(<span class="hljs-string">'*'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> context = { url: req.url };

  <span class="hljs-comment">// Render our Vue app to a string</span>
  renderer.renderToString(context, <span class="hljs-function">(<span class="hljs-params">err, html</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (err) {
      <span class="hljs-comment">// Handle error</span>
      res.status(<span class="hljs-number">500</span>).end(<span class="hljs-string">'Server Error'</span>);
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-comment">// Send the rendered HTML to the client</span>
    res.end(html);
  });
});

server.listen(<span class="hljs-number">8080</span>);
</code></pre>
<pre><code class="lang-typescript">&lt;!-- index.template.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;Server-side Rendering Example&lt;/title&gt;
  &lt;!-- Resources injected by the server renderer --&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- This will be replaced <span class="hljs-keyword">with</span> the app<span class="hljs-string">'s HTML --&gt;
  &lt;!--vue-ssr-outlet--&gt;
&lt;/body&gt;
&lt;/html&gt;</span>
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// entry-server.js</span>
<span class="hljs-keyword">import</span> { createApp } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> context =&gt; {
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { app, router } = createApp();

    <span class="hljs-comment">// Set server-side router's location</span>
    router.push(context.url);

    <span class="hljs-comment">// Wait until router has resolved possible async components and hooks</span>
    router.onReady(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">const</span> matchedComponents = router.getMatchedComponents();

      <span class="hljs-comment">// No matched routes, reject with 404</span>
      <span class="hljs-keyword">if</span> (!matchedComponents.length) {
        <span class="hljs-keyword">return</span> reject({ code: <span class="hljs-number">404</span> });
      }

      <span class="hljs-comment">// The Promise resolves to the app instance</span>
      resolve(app);
    }, reject);
  });
}
</code></pre>
<p>From the two processes above, you can see that the difference lies in the second step. A client-rendered website will directly return the HTML file, while a server-rendered website will render the page completely before returning this HTML file.</p>
<h4 id="heading-whats-the-benefit-of-doing-this-its-a-faster-time-to-content">What's the benefit of doing this? It's a faster time-to-content.</h4>
<p>Suppose your website needs to load four files (a, b, c, d) to render completely. And each file is 1 MB in size.</p>
<p>Calculating this way: a client-rendered website needs to load 4 files and an HTML file to complete the home page rendering, totaling 4MB (ignoring the HTML file size). While a server-rendered website only needs to load a fully rendered HTML file to complete the home page rendering, totaling the size of the already rendered HTML file (which isn't usually too large, generally a few hundred KB; my personal blog website (SSR) loads an HTML file of 400KB). <strong>This is why server-side rendering is faster.</strong></p>
<p>References:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/woai3c/vue-ssr-demo">vue-ssr-demo</a></p>
</li>
<li><p><a target="_blank" href="https://ssr.vuejs.org/zh/">Vue.js Server-Side Rendering Guide</a></p>
</li>
</ul>
<h2 id="heading-4-use-a-cdn-for-static-resources"><strong>4. Use a CDN for Static Resources</strong></h2>
<p>A Content Delivery Network (CDN) is a set of web servers distributed across multiple geographic locations. We all know that the further the server is from the user, the higher the latency. CDNs are designed to solve this problem by deploying servers in multiple locations, bringing users closer to servers, thereby shortening request times.</p>
<h3 id="heading-cdn-principles">CDN Principles</h3>
<p>When a user visits a website without a CDN, the process is as follows:</p>
<ol>
<li><p>The browser needs to resolve the domain name into an IP address, so it makes a request to the local DNS.</p>
</li>
<li><p>The local DNS makes successive requests to the root server, top-level domain server, and authoritative server to get the IP address of the website's server.</p>
</li>
<li><p>The local DNS sends the IP address back to the browser, and the browser makes a request to the website server's IP address and receives the resources.</p>
</li>
</ol>
<p><a target="_blank" href="https://camo.githubusercontent.com/a9d8ea319521e8f560e8b68c2df8a4afaf27ed46e29e481b35bb78d013d23ca6/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f7a3079336a387a733733727a62617466616731342e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0y3j8zs73rzbatfag14.png" alt="Diagram showing request flow without CDN: browser → DNS → root servers → top-level domain → authoritative server → website server" width="600" height="400" loading="lazy"></a></p>
<p>If the user is visiting a website that has deployed a CDN, the process is as follows:</p>
<ol>
<li><p>The browser needs to resolve the domain name into an IP address, so it makes a request to the local DNS.</p>
</li>
<li><p>The local DNS makes successive requests to the root server, top-level domain server, and authoritative server to get the IP address of the Global Server Load Balancing (GSLB) system.</p>
</li>
<li><p>The local DNS then makes a request to the GSLB. The main function of the GSLB is to determine the user's location based on the local DNS's IP address, filter out the closest local Server Load Balancing (SLB) system to the user, and return the IP address of that SLB to the local DNS.</p>
</li>
<li><p>The local DNS sends the SLB's IP address back to the browser, and the browser makes a request to the SLB.</p>
</li>
<li><p>The SLB selects the optimal cache server based on the resource and address requested by the browser and sends it back to the browser.</p>
</li>
<li><p>The browser then redirects to the cache server based on the address returned by the SLB.</p>
</li>
<li><p>If the cache server has the resource the browser needs, it sends the resource back to the browser. If not, it requests the resource from the source server, sends it to the browser, and caches it locally.</p>
</li>
</ol>
<p><a target="_blank" href="https://camo.githubusercontent.com/1ade29f05689af94c1066bccedab884a119d2fb4cba44f08fd95357cd9abdef6/68747470733a2f2f6465762d746f2d75706c6f6164732e73332e616d617a6f6e6177732e636f6d2f75706c6f6164732f61727469636c65732f616f70776c68783778386f33726176766e3170322e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faopwlhx7x8o3ravvn1p2.png" alt="Diagram showing request flow with CDN: browser → DNS → root servers → GSLB → SLB → cache servers → origin server" width="600" height="400" loading="lazy"></a></p>
<p>References:</p>
<ul>
<li><p><a target="_blank" href="https://en.wikipedia.org/wiki/Content_delivery_network">Content delivery network(CDN)</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/how-cdns-improve-performance-in-front-end-projects/">How to use CDNs to improve performance</a></p>
</li>
</ul>
<h2 id="heading-5-place-css-in-the-head-and-javascript-files-at-the-bottom"><strong>5. Place CSS in the Head and JavaScript Files at the Bottom</strong></h2>
<ul>
<li><p>CSS execution blocks rendering and prevents JS execution</p>
</li>
<li><p>JS loading and execution block HTML parsing and prevent CSSOM construction</p>
</li>
</ul>
<p>If these CSS and JS tags are placed in the HEAD tag, and they take a long time to load and parse, then the page will be blank. So you should place JS files at the bottom (not blocking DOM parsing but will block rendering) so that HTML parsing is completed before loading JS files. This presents the page content to the user as early as possible.</p>
<p>So then you might be wondering – why should CSS files still be placed in the head?</p>
<p>Because loading HTML first and then loading CSS will make users see an unstyled, "ugly" page at first glance. To avoid this situation, place CSS files in the head.</p>
<p>You can also place JS files in the head as long as the script tag has the defer attribute, which means asynchronous download and delayed execution.</p>
<p><strong>Here's an example of optimal placement:</strong></p>
<pre><code class="lang-typescript">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta charset=<span class="hljs-string">"UTF-8"</span>&gt;
  &lt;title&gt;Optimized Resource Loading&lt;/title&gt;

  &lt;!-- CSS <span class="hljs-keyword">in</span> the head <span class="hljs-keyword">for</span> faster rendering --&gt;
  &lt;link rel=<span class="hljs-string">"stylesheet"</span> href=<span class="hljs-string">"styles.css"</span>&gt;

  &lt;!-- Critical JS that must load early can use defer --&gt;
  &lt;script defer src=<span class="hljs-string">"critical.js"</span>&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;header&gt;
    &lt;h1&gt;My Website&lt;/h1&gt;
    &lt;!-- Page content here --&gt;
  &lt;/header&gt;

  &lt;main&gt;
    &lt;p&gt;Content that users need to see quickly...&lt;/p&gt;
  &lt;/main&gt;

  &lt;footer&gt;
    &lt;!-- Footer content --&gt;
  &lt;/footer&gt;

  &lt;!-- Non-critical JavaScript at the bottom --&gt;
  &lt;script src=<span class="hljs-string">"app.js"</span>&gt;&lt;/script&gt;
  &lt;script src=<span class="hljs-string">"analytics.js"</span>&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p><strong>Explanation of this approach:</strong></p>
<ol>
<li><p><strong>CSS in the</strong> <code>&lt;head&gt;</code>: Ensures the page is styled as soon as it renders, preventing the "flash of unstyled content" (FOUC). CSS is render-blocking, but that's actually what we want in this case.</p>
</li>
<li><p><strong>Critical JS with</strong> <code>defer</code>: The <code>defer</code> attribute tells the browser to:</p>
<ul>
<li><p>Download the script in parallel while parsing HTML</p>
</li>
<li><p>Only execute the script after HTML parsing is complete but before the <code>DOMContentLoaded</code> event</p>
</li>
<li><p>Maintain the order of execution if there are multiple deferred scripts</p>
</li>
</ul>
</li>
<li><p><strong>Non-critical JS before closing</strong> <code>&lt;/body&gt;</code>: Scripts without special attributes will:</p>
<ul>
<li><p>Block HTML parsing while they download and execute</p>
</li>
<li><p>By placing them at the bottom, we ensure that all the important content is parsed and displayed first</p>
</li>
<li><p>This improves perceived performance even if the total load time is the same</p>
</li>
</ul>
</li>
</ol>
<p>You can also use <code>async</code> for scripts that don't depend on DOM or other scripts:</p>
<pre><code class="lang-typescript">&lt;script <span class="hljs-keyword">async</span> src=<span class="hljs-string">"independent.js"</span>&gt;&lt;/script&gt;
</code></pre>
<p>The <code>async</code> attribute will download the script in parallel and execute it as soon as it's available, which may interrupt HTML parsing. Use this only for scripts that don't modify the DOM or depend on other scripts.</p>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Your_first_website/Adding_interactivity">Adding Interactivity with JavaScript</a></li>
</ul>
<h2 id="heading-6-use-font-icons-iconfont-instead-of-image-icons"><strong>6. Use Font Icons (iconfont) Instead of Image Icons</strong></h2>
<p>A font icon is an icon made into a font. When using it, it's just like a font, and you can set attributes such as font-size, color, and so on, which is very convenient. Font icons are also vector graphics and won't lose clarity. Another advantage is that the generated files are particularly small.</p>
<h3 id="heading-compress-font-files">Compress Font Files</h3>
<p>Use the <a target="_blank" href="https://github.com/patrickhulce/fontmin-webpack">fontmin-webpack</a> plugin to compress font files (thanks to <a target="_blank" href="https://juejin.im/user/237150239985165">Frontend Xiaowei</a> for providing this).</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/8aec44850415bdf6f23aa59cae5daa0c6d06ec9414766ddfe34c294b663fcde4/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f37376232656235653365303933323030383765333337303638366461393330302e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmbq5m02e5myhbyz7c5d.png" alt="Showing difference between uncompressed and compressed files" width="600" height="400" loading="lazy"></a></p>
<p>References:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/patrickhulce/fontmin-webpack">fontmin-webpack</a></p>
</li>
<li><p><a target="_blank" href="https://www.iconfont.cn/">Iconfont-Alibaba Vector Icon Library</a></p>
</li>
</ul>
<h2 id="heading-7-make-good-use-of-caching-avoid-reloading-the-same-resources"><strong>7. Make Good Use of Caching, Avoid Reloading the Same Resources</strong></h2>
<p>To prevent users from having to request files every time they visit a website, we can control this behavior by adding Expires or max-age. Expires sets a time, and as long as it's before this time, the browser won't request the file but will directly use the cache. Max-age is a relative time, and it's recommended to use max-age instead of Expires.</p>
<p>But this creates a problem: what happens when the file is updated? How do we notify the browser to request the file again?</p>
<p>This can be done by updating the resource link addresses referenced in the page, making the browser actively abandon the cache and load new resources.</p>
<p>The specific approach is to associate the URL modification of the resource address with the file content, which means that only when the file content changes, the corresponding URL will change. This achieves file-level precise cache control.</p>
<p>So what is related to file content? We naturally think of using <a target="_blank" href="https://www.okta.com/identity-101/md5/">digest algorithms</a> to derive digest information for the file. The digest information corresponds one-to-one with the file content, providing a basis for cache control that's precise to the granularity of individual files.</p>
<h3 id="heading-how-to-implement-caching-and-cache-busting"><strong>How to implement caching and cache-busting:</strong></h3>
<p><strong>1. Server-side cache headers (using Express.js as an example):</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Set cache control headers for static resources</span>
app.use(<span class="hljs-string">'/static'</span>, express.static(<span class="hljs-string">'public'</span>, {
  maxAge: <span class="hljs-string">'1y'</span>, <span class="hljs-comment">// Cache for 1 year</span>
  etag: <span class="hljs-literal">true</span>,   <span class="hljs-comment">// Use ETag for validation</span>
  lastModified: <span class="hljs-literal">true</span> <span class="hljs-comment">// Use Last-Modified for validation</span>
}));

<span class="hljs-comment">// For HTML files that shouldn't be cached as long</span>
app.get(<span class="hljs-string">'/*.html'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.set({
    <span class="hljs-string">'Cache-Control'</span>: <span class="hljs-string">'public, max-age=300'</span>, <span class="hljs-comment">// Cache for 5 minutes</span>
    <span class="hljs-string">'Expires'</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">300000</span>).toUTCString()
  });
  <span class="hljs-comment">// Send HTML content</span>
});
</code></pre>
<p><strong>2. Using content hashes in filenames (Webpack configuration):</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// webpack.config.js</span>
<span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = {
  output: {
    filename: <span class="hljs-string">'[name].[contenthash].js'</span>, <span class="hljs-comment">// Uses content hash in filename</span>
    path: path.resolve(__dirname, <span class="hljs-string">'dist'</span>),
  },
  plugins: [
    <span class="hljs-comment">// Extract CSS into separate files with content hash</span>
    <span class="hljs-keyword">new</span> MiniCssExtractPlugin({
      filename: <span class="hljs-string">'[name].[contenthash].css'</span>
    }),
    <span class="hljs-comment">// Generate HTML with correct hashed filenames</span>
    <span class="hljs-keyword">new</span> HtmlWebpackPlugin({
      template: <span class="hljs-string">'src/index.html'</span>
    })
  ]
};
</code></pre>
<p>This will produce output files like:</p>
<ul>
<li><p><code>main.8e0d62a10c151dad4f8e.js</code></p>
</li>
<li><p><code>styles.f4e3a77c616562b26ca1.css</code></p>
</li>
</ul>
<p>When you change the content of a file, its hash will change, forcing the browser to download the new file instead of using the cached version.</p>
<p><strong>3. Example of generated HTML with cache-busting:</strong></p>
<pre><code class="lang-typescript">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta charset=<span class="hljs-string">"UTF-8"</span>&gt;
  &lt;title&gt;Cache Busting Example&lt;/title&gt;
  &lt;!-- Note the content hash <span class="hljs-keyword">in</span> the filename --&gt;
  &lt;link rel=<span class="hljs-string">"stylesheet"</span> href=<span class="hljs-string">"/static/styles.f4e3a77c616562b26ca1.css"</span>&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id=<span class="hljs-string">"app"</span>&gt;&lt;/div&gt;
  &lt;!-- Script <span class="hljs-keyword">with</span> content hash --&gt;
  &lt;script src=<span class="hljs-string">"/static/main.8e0d62a10c151dad4f8e.js"</span>&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p><strong>4. Version query parameters (simpler but less effective approach):</strong></p>
<pre><code class="lang-typescript">&lt;link rel=<span class="hljs-string">"stylesheet"</span> href=<span class="hljs-string">"styles.css?v=1.2.3"</span>&gt;
&lt;script src=<span class="hljs-string">"app.js?v=1.2.3"</span>&gt;&lt;/script&gt;
</code></pre>
<p>When updating files, manually change the version number to force a new download.</p>
<p>References:</p>
<ul>
<li><a target="_blank" href="https://webpack.js.org/guides/caching/#root">webpack-caching</a></li>
</ul>
<h2 id="heading-8-compress-files"><strong>8. Compress Files</strong></h2>
<p>Compressing files can reduce file download time, providing a better user experience.</p>
<p>Thanks to the development of Webpack and Node, file compression is now very convenient.</p>
<p>In Webpack, you can use the following plugins for compression:</p>
<ul>
<li><p>JavaScript: UglifyPlugin</p>
</li>
<li><p>CSS: MiniCssExtractPlugin</p>
</li>
<li><p>HTML: HtmlWebpackPlugin</p>
</li>
</ul>
<p>In fact, we can do even better by using gzip compression. This can be enabled by adding the gzip identifier to the Accept-Encoding header in the HTTP request header. Of course, the server must also support this feature.</p>
<p>Gzip is currently the most popular and effective compression method. For example, the app.js file generated after building a project I developed with Vue has a size of 1.4MB, but after gzip compression, it's only 573KB, reducing the volume by nearly 60%.</p>
<p>Here are the methods for configuring gzip in webpack and node.</p>
<p><strong>Download plugins</strong></p>
<pre><code class="lang-typescript">npm install compression-webpack-plugin --save-dev
npm install compression
</code></pre>
<p><strong>Webpack configuration</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> CompressionPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'compression-webpack-plugin'</span>);

<span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = {
  plugins: [<span class="hljs-keyword">new</span> CompressionPlugin()],
}
</code></pre>
<p><strong>Node configuration</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> compression = <span class="hljs-built_in">require</span>(<span class="hljs-string">'compression'</span>)
<span class="hljs-comment">// Use before other middleware</span>
app.use(compression())
</code></pre>
<h2 id="heading-9-image-optimization"><strong>9. Image Optimization</strong></h2>
<h3 id="heading-1-lazy-loading-images"><strong>1. Lazy Loading Images</strong></h3>
<p>In a page, don't initially set the path for images – only load the actual image when it appears in the browser's viewport. This is lazy loading. For websites with many images, loading all images at once can have a significant impact on user experience, so image lazy loading is necessary.</p>
<p>First, set up the images like this, where images won't load when they're not visible in the page:</p>
<pre><code class="lang-typescript">&lt;img data-src=<span class="hljs-string">"https://avatars0.githubusercontent.com/u/22117876?s=460&amp;u=7bd8f32788df6988833da6bd155c3cfbebc68006&amp;v=4"</span>&gt;
</code></pre>
<p>When the page becomes visible, use JS to load the image:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> img = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'img'</span>)
img.src = img.dataset.src
</code></pre>
<p>This is how the image gets loaded. For the complete code, please refer to the reference materials.</p>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading">Lazy loading images for the web</a></li>
</ul>
<h3 id="heading-2-responsive-images"><strong>2. Responsive Images</strong></h3>
<p>The advantage of responsive images is that browsers can automatically load appropriate images based on screen size.</p>
<p>Implementation through <code>picture</code>:</p>
<pre><code class="lang-typescript">&lt;picture&gt;
    &lt;source srcset=<span class="hljs-string">"banner_w1000.jpg"</span> media=<span class="hljs-string">"(min-width: 801px)"</span>&gt;
    &lt;source srcset=<span class="hljs-string">"banner_w800.jpg"</span> media=<span class="hljs-string">"(max-width: 800px)"</span>&gt;
    &lt;img src=<span class="hljs-string">"banner_w800.jpg"</span> alt=<span class="hljs-string">""</span>&gt;
&lt;/picture&gt;
</code></pre>
<p>Implementation through <code>@media</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-meta">@media</span> (min-width: <span class="hljs-number">769</span>px) {
    .bg {
        background-image: url(bg1080.jpg);
    }
}
<span class="hljs-meta">@media</span> (max-width: <span class="hljs-number">768</span>px) {
    .bg {
        background-image: url(bg768.jpg);
    }
}
</code></pre>
<h3 id="heading-3-adjust-image-size"><strong>3. Adjust Image Size</strong></h3>
<p>For example, if you have a 1920 * 1080 size image, you show it to users as a thumbnail, and only display the full image when users hover over it. If users never actually hover over the thumbnail, the time spent downloading the image is wasted.</p>
<p>So we can optimize this with two images. Initially, only load the thumbnail, and when users hover over the image, then load the large image. Another approach is to lazy load the large image, manually changing the src of the large image to download it after all elements have loaded.</p>
<p><strong>Example implementation of image size optimization:</strong></p>
<pre><code class="lang-typescript">&lt;!-- HTML Structure --&gt;
&lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"image-container"</span>&gt;
  &lt;img <span class="hljs-keyword">class</span>=<span class="hljs-string">"thumbnail"</span> src=<span class="hljs-string">"thumbnail-small.jpg"</span> alt=<span class="hljs-string">"Small thumbnail"</span>&gt;
  &lt;img <span class="hljs-keyword">class</span>=<span class="hljs-string">"full-size"</span> data-src=<span class="hljs-string">"image-large.jpg"</span> alt=<span class="hljs-string">"Full-size image"</span>&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">/* CSS for the container and images */</span>
.image-container {
  position: relative;
  width: <span class="hljs-number">200</span>px;
  height: <span class="hljs-number">150</span>px;
  overflow: hidden;
}

.thumbnail {
  width: <span class="hljs-number">100</span>%;
  height: <span class="hljs-number">100</span>%;
  <span class="hljs-built_in">object</span>-fit: cover;
  display: block;
}

.full-size {
  display: none;
  position: absolute;
  top: <span class="hljs-number">0</span>;
  left: <span class="hljs-number">0</span>;
  z-index: <span class="hljs-number">2</span>;
  max-width: <span class="hljs-number">600</span>px;
  max-height: <span class="hljs-number">400</span>px;
}

<span class="hljs-comment">/* Show full size on hover */</span>
.image-container:hover .full-size {
  display: block;
}
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// JavaScript to lazy load the full-size image</span>
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'DOMContentLoaded'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> containers = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'.image-container'</span>);

  containers.forEach(<span class="hljs-function"><span class="hljs-params">container</span> =&gt;</span> {
    <span class="hljs-keyword">const</span> thumbnail = container.querySelector(<span class="hljs-string">'.thumbnail'</span>);
    <span class="hljs-keyword">const</span> fullSize = container.querySelector(<span class="hljs-string">'.full-size'</span>);

    <span class="hljs-comment">// Load the full-size image when the user hovers over the thumbnail</span>
    container.addEventListener(<span class="hljs-string">'mouseenter'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (!fullSize.src &amp;&amp; fullSize.dataset.src) {
        fullSize.src = fullSize.dataset.src;
      }
    });

    <span class="hljs-comment">// Alternative: Load the full-size image after the page loads completely</span>
    <span class="hljs-comment">/*
    window.addEventListener('load', () =&gt; {
      setTimeout(() =&gt; {
        if (!fullSize.src &amp;&amp; fullSize.dataset.src) {
          fullSize.src = fullSize.dataset.src;
        }
      }, 1000); // Delay loading by 1 second after window load
    });
    */</span>
  });
});
</code></pre>
<p>This implementation:</p>
<ol>
<li><p>Shows only the thumbnail initially</p>
</li>
<li><p>Loads the full-size image only when the user hovers over the thumbnail</p>
</li>
<li><p>Provides an alternative approach to load all full-size images with a delay after page load</p>
</li>
</ol>
<h3 id="heading-4-reduce-image-quality"><strong>4. Reduce Image Quality</strong></h3>
<p>For example, with JPG format images, there's usually no noticeable difference between 100% quality and 90% quality, especially when used as background images. When cutting background images in Adobe Photoshop, I often cut the image into JPG format and compress it to 60% quality, and basically can't see any difference.</p>
<p>There are two compression methods: one is through the Webpack plugin <code>image-webpack-loader</code>, and the other is through online compression websites.</p>
<p>Here's how to use the Webpack plugin <code>image-webpack-loader</code>:</p>
<pre><code class="lang-typescript">npm i -D image-webpack-loader
</code></pre>
<p>Webpack configuration:</p>
<pre><code class="lang-typescript">{
  test: <span class="hljs-regexp">/\.(png|jpe?g|gif|svg)(\?.*)?$/</span>,
  use:[
    {
    loader: <span class="hljs-string">'url-loader'</span>,
    options: {
      limit: <span class="hljs-number">10000</span>, <span class="hljs-comment">/* Images smaller than 1000 bytes will be automatically converted to base64 code references */</span>
      name: utils.assetsPath(<span class="hljs-string">'img/[name].[hash:7].[ext]'</span>)
      }
    },
    <span class="hljs-comment">/* Compress images */</span>
    {
      loader: <span class="hljs-string">'image-webpack-loader'</span>,
      options: {
        bypassOnDebug: <span class="hljs-literal">true</span>,
      }
    }
  ]
}
</code></pre>
<h3 id="heading-5-use-css3-effects-instead-of-images-when-possible"><strong>5. Use CSS3 Effects Instead of Images When Possible</strong></h3>
<p>Many images can be drawn with CSS effects (gradients, shadows, and so on). In these cases, CSS3 effects are better. This is because code size is usually a fraction or even a tenth of the image size.</p>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://webpack.js.org/guides/asset-management/">Asset Management</a></li>
</ul>
<h3 id="heading-6-use-webp-to-format-images"><strong>6. Use WebP to Format Images</strong></h3>
<p>WebP's advantage is reflected in its better image data compression algorithm, which brings smaller image volume while maintaining image quality that's indistinguishable to the naked eye. It also has lossless and lossy compression modes, Alpha transparency, and animation features. Its conversion effects on JPEG and PNG are quite excellent, stable, and uniform.</p>
<p><strong>Example of implementing WebP with fallbacks:</strong></p>
<pre><code class="lang-typescript">&lt;!-- Using the picture element <span class="hljs-keyword">for</span> WebP <span class="hljs-keyword">with</span> fallback --&gt;
&lt;picture&gt;
  &lt;source srcset=<span class="hljs-string">"image.webp"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"image/webp"</span>&gt;
  &lt;source srcset=<span class="hljs-string">"image.jpg"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"image/jpeg"</span>&gt;
  &lt;img src=<span class="hljs-string">"image.jpg"</span> alt=<span class="hljs-string">"Description of the image"</span>&gt;
&lt;/picture&gt;
</code></pre>
<p><strong>Server-side WebP detection and serving:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Express.js example</span>
app.get(<span class="hljs-string">'/images/:imageName'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> supportsWebP = req.headers.accept &amp;&amp; req.headers.accept.includes(<span class="hljs-string">'image/webp'</span>);
  <span class="hljs-keyword">const</span> imagePath = supportsWebP 
    ? <span class="hljs-string">`public/images/<span class="hljs-subst">${req.params.imageName}</span>.webp`</span> 
    : <span class="hljs-string">`public/images/<span class="hljs-subst">${req.params.imageName}</span>.jpg`</span>;

  res.sendFile(path.resolve(__dirname, imagePath));
});
</code></pre>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/WebP">WebP</a></li>
</ul>
<h2 id="heading-10-load-code-on-demand-through-webpack-extract-third-party-libraries-reduce-redundant-code-when-converting-es6-to-es5"><strong>10. Load Code on Demand Through Webpack, Extract Third-Party Libraries, Reduce Redundant Code When Converting ES6 to ES5</strong></h2>
<p>The following quote from the official Webpack documentation explains the concept of lazy loading:</p>
<blockquote>
<p>"Lazy loading or on-demand loading is a great way to optimize a website or application. This approach actually separates your code at some logical breakpoints, and then immediately references or is about to reference some new code blocks after completing certain operations in some code blocks. This speeds up the initial loading of the application and lightens its overall volume because some code blocks may never be loaded." <em>Source:</em> <a target="_blank" href="http://webpack.docschina.org/guides/lazy-loading/"><em>Lazy Loading</em></a></p>
</blockquote>
<p><strong>Note:</strong> While image lazy loading (discussed in section 9.1) delays the loading of image resources until they're visible in the viewport, code lazy loading splits JavaScript bundles and loads code fragments only when they're needed for specific functionality. They both improve initial load time, but they work at different levels of resource optimization.</p>
<h3 id="heading-generate-file-names-based-on-file-content-combined-with-import-dynamic-import-of-components-to-achieve-on-demand-loading"><strong>Generate File Names Based on File Content, Combined with Import Dynamic Import of Components to Achieve On-Demand Loading</strong></h3>
<p>This requirement can be achieved by configuring the filename property of output. One of the value options in the filename property is <code>[contenthash]</code>, which creates a unique hash based on file content. When the file content changes, <code>[contenthash]</code> also changes.</p>
<pre><code class="lang-typescript">output: {
    filename: <span class="hljs-string">'[name].[contenthash].js'</span>,
    chunkFilename: <span class="hljs-string">'[name].[contenthash].js'</span>,
    path: path.resolve(__dirname, <span class="hljs-string">'../dist'</span>),
},
</code></pre>
<p><strong>Example of code lazy loading in a Vue application:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Instead of importing synchronously like this:</span>
<span class="hljs-comment">// import UserProfile from './components/UserProfile.vue'</span>

<span class="hljs-comment">// Use dynamic import for route components:</span>
<span class="hljs-keyword">const</span> UserProfile = <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">'./components/UserProfile.vue'</span>)

<span class="hljs-comment">// Then use it in your routes</span>
<span class="hljs-keyword">const</span> router = <span class="hljs-keyword">new</span> VueRouter({
  routes: [
    { path: <span class="hljs-string">'/user/:id'</span>, component: UserProfile }
  ]
})
</code></pre>
<p>This ensures the UserProfile component is only loaded when a user navigates to that route, not on initial page load.</p>
<h3 id="heading-extract-third-party-libraries"><strong>Extract Third-Party Libraries</strong></h3>
<p>Since imported third-party libraries are generally stable and don't change frequently, extracting them separately as long-term caches is a better choice. This requires using the cacheGroups option of Webpack4's splitChunk plugin.</p>
<pre><code class="lang-typescript">optimization: {
    runtimeChunk: {
        name: <span class="hljs-string">'manifest'</span> <span class="hljs-comment">// Split webpack's runtime code into a separate chunk.</span>
    },
    splitChunks: {
        cacheGroups: {
            vendor: {
                name: <span class="hljs-string">'chunk-vendors'</span>,
                test: <span class="hljs-regexp">/[\\/]node_modules[\\/]/</span>,
                priority: <span class="hljs-number">-10</span>,
                chunks: <span class="hljs-string">'initial'</span>
            },
            common: {
                name: <span class="hljs-string">'chunk-common'</span>,
                minChunks: <span class="hljs-number">2</span>,
                priority: <span class="hljs-number">-20</span>,
                chunks: <span class="hljs-string">'initial'</span>,
                reuseExistingChunk: <span class="hljs-literal">true</span>
            }
        },
    }
},
</code></pre>
<ul>
<li><p><code>test</code>: Used to control which modules are matched by this cache group. If passed unchanged, it defaults to select all modules. Types of values that can be passed: <code>RegExp</code>, <code>String</code>, and <code>Function</code>.</p>
</li>
<li><p><code>priority</code>: Indicates extraction weight, with higher numbers indicating higher priority. Since a module might meet the conditions of multiple <code>cacheGroups</code>, extraction is determined by the highest weight.</p>
</li>
<li><p><code>reuseExistingChunk</code>: Indicates whether to use existing chunks. If true, it means that if the current chunk contains modules that have already been extracted, new ones won't be generated.</p>
</li>
<li><p><code>minChunks</code> (default is 1): The minimum number of times this code block should be referenced before splitting (note: to ensure code block reusability, the default strategy doesn't require multiple references to be split).</p>
</li>
<li><p><code>chunks</code> (default is async): initial, async, and all.</p>
</li>
<li><p><code>name</code> (name of the packaged chunks): String or function (functions can customize names based on conditions).</p>
</li>
</ul>
<h3 id="heading-reduce-redundant-code-when-converting-es6-to-es5"><strong>Reduce Redundant Code When Converting ES6 to ES5</strong></h3>
<p>To achieve the same functionality as the original code after Babel conversion, some helper functions are needed. For example this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> Person {}
</code></pre>
<p>will be converted to this:</p>
<pre><code class="lang-typescript"><span class="hljs-meta">"use strict"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_classCallCheck</span>(<span class="hljs-params">instance, Constructor</span>) </span>{
  <span class="hljs-keyword">if</span> (!(instance <span class="hljs-keyword">instanceof</span> Constructor)) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">TypeError</span>(<span class="hljs-string">"Cannot call a class as a function"</span>);
  }
}

<span class="hljs-keyword">var</span> Person = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Person</span>(<span class="hljs-params"></span>) </span>{
  _classCallCheck(<span class="hljs-built_in">this</span>, Person);
};
</code></pre>
<p>Here, <code>_classCallCheck</code> is a <code>helper</code> function. If classes are declared in many files, then many such <code>helper</code> functions will be generated.</p>
<p>The <code>@babel/runtime</code> package declares all the helper functions needed, and the role of <code>@babel/plugin-transform-runtime</code> is to import all files that need <code>helper</code> functions from the <code>@babel/runtime package</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-meta">"use strict"</span>;

<span class="hljs-keyword">var</span> _classCallCheck2 = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@babel/runtime/helpers/classCallCheck"</span>);

<span class="hljs-keyword">var</span> _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_interopRequireDefault</span>(<span class="hljs-params">obj</span>) </span>{
  <span class="hljs-keyword">return</span> obj &amp;&amp; obj.__esModule ? obj : { <span class="hljs-keyword">default</span>: obj };
}

<span class="hljs-keyword">var</span> Person = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Person</span>(<span class="hljs-params"></span>) </span>{
  (<span class="hljs-number">0</span>, _classCallCheck3.default)(<span class="hljs-built_in">this</span>, Person);
};
</code></pre>
<p>Here, the <code>helper</code> function <code>classCallCheck</code> is no longer compiled, but instead references <code>helpers/classCallCheck</code> from <code>@babel/runtime</code>.</p>
<p><strong>Installation:</strong></p>
<pre><code class="lang-typescript">npm i -D <span class="hljs-meta">@babel</span>/plugin-transform-runtime <span class="hljs-meta">@babel</span>/runtime
</code></pre>
<p><strong>Usage:</strong><br>In the <code>.babelrc</code> file,</p>
<pre><code class="lang-typescript"><span class="hljs-string">"plugins"</span>: [
        <span class="hljs-string">"@babel/plugin-transform-runtime"</span>
]
</code></pre>
<p>References:</p>
<ul>
<li><p><a target="_blank" href="https://babeljs.io/">Babel</a></p>
</li>
<li><p><a target="_blank" href="https://router.vuejs.org/guide/advanced/lazy-loading.html">Vue Route Lazy Loading</a></p>
</li>
<li><p><a target="_blank" href="https://webpack.js.org/plugins/split-chunks-plugin/">SplitChunksPlugin</a></p>
</li>
</ul>
<h2 id="heading-11-reduce-reflows-and-repaints"><strong>11. Reduce Reflows and Repaints</strong></h2>
<h3 id="heading-browser-rendering-process"><strong>Browser Rendering Process</strong></h3>
<ol>
<li><p>Parse HTML to generate DOM tree.</p>
</li>
<li><p>Parse CSS to generate CSSOM rules tree.</p>
</li>
<li><p>Combine DOM tree and CSSOM rules tree to generate rendering tree.</p>
</li>
<li><p>Traverse the rendering tree to begin layout, calculating the position and size information of each node.</p>
</li>
<li><p>Paint each node of the rendering tree to the screen.</p>
</li>
</ol>
<p><a target="_blank" href="https://camo.githubusercontent.com/b01f818aab6cf14622f77ee3d2407b961b38b4654ab88c3fa391d2b43a77c46c/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f35363437643961643461643561353731373839313964656165353137356238332e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7yfvoxwqdvs7a9c6v9b.png" alt="Diagram of browser rendering process showing the steps from HTML/CSS to rendered pixels" width="600" height="400" loading="lazy"></a></p>
<h3 id="heading-reflow"><strong>Reflow</strong></h3>
<p>When the position or size of DOM elements is changed, the browser needs to regenerate the rendering tree, a process called reflow.</p>
<h3 id="heading-repaint"><strong>Repaint</strong></h3>
<p>After regenerating the rendering tree, each node of the rendering tree needs to be painted to the screen, a process called repaint. Not all actions will cause reflow – for example, changing font color will only cause repaint. Remember, reflow will cause repaint, but repaint will not cause reflow.</p>
<p>Both reflow and repaint operations are very expensive because the JavaScript engine thread and the GUI rendering thread are mutually exclusive, and only one can work at a time.</p>
<p>What operations will cause reflow?</p>
<ul>
<li><p>Adding or removing visible DOM elements</p>
</li>
<li><p>Element position changes</p>
</li>
<li><p>Element size changes</p>
</li>
<li><p>Content changes</p>
</li>
<li><p>Browser window size changes</p>
</li>
</ul>
<p>How to reduce reflows and repaints?</p>
<ul>
<li><p>When modifying styles with JavaScript, it's best not to write styles directly, but to replace classes to change styles.</p>
</li>
<li><p>If you need to perform a series of operations on a DOM element, you can take the DOM element out of the document flow, make modifications, and then bring it back to the document. It's recommended to use hidden elements (display:none) or document fragments (DocumentFragement), both of which can implement this approach well.</p>
</li>
</ul>
<p><strong>Example of causing unnecessary reflows (inefficient):</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// This causes multiple reflows as each style change triggers a reflow</span>
<span class="hljs-keyword">const</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myElement'</span>);
element.style.width = <span class="hljs-string">'100px'</span>;
element.style.height = <span class="hljs-string">'200px'</span>;
element.style.margin = <span class="hljs-string">'10px'</span>;
element.style.padding = <span class="hljs-string">'20px'</span>;
element.style.borderRadius = <span class="hljs-string">'5px'</span>;
</code></pre>
<p><strong>Optimized version 1 – using CSS classes:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">/* style.css */</span>
.my-modified-element {
  width: <span class="hljs-number">100</span>px;
  height: <span class="hljs-number">200</span>px;
  margin: <span class="hljs-number">10</span>px;
  padding: <span class="hljs-number">20</span>px;
  border-radius: <span class="hljs-number">5</span>px;
}
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// Only one reflow happens when the class is added</span>
<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myElement'</span>).classList.add(<span class="hljs-string">'my-modified-element'</span>);
</code></pre>
<p><strong>Optimized version 2 – batching style changes:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Batching style changes using cssText</span>
<span class="hljs-keyword">const</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myElement'</span>);
element.style.cssText = <span class="hljs-string">'width: 100px; height: 200px; margin: 10px; padding: 20px; border-radius: 5px;'</span>;
</code></pre>
<p><strong>Optimized version 3 – using document fragments (for multiple elements):</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Instead of adding elements one by one</span>
<span class="hljs-keyword">const</span> list = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myList'</span>);
<span class="hljs-keyword">const</span> fragment = <span class="hljs-built_in">document</span>.createDocumentFragment();

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++) {
  <span class="hljs-keyword">const</span> item = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'li'</span>);
  item.textContent = <span class="hljs-string">`Item <span class="hljs-subst">${i}</span>`</span>;
  fragment.appendChild(item);
}

<span class="hljs-comment">// Only one reflow happens when the fragment is appended</span>
list.appendChild(fragment);
</code></pre>
<p><strong>Optimized version 4 – take element out of flow, modify, then reinsert:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Remove from DOM, make changes, then reinsert</span>
<span class="hljs-keyword">const</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myElement'</span>);
<span class="hljs-keyword">const</span> parent = element.parentNode;
<span class="hljs-keyword">const</span> nextSibling = element.nextSibling;

<span class="hljs-comment">// Remove (causes one reflow)</span>
parent.removeChild(element);

<span class="hljs-comment">// Make multiple changes (no reflows while detached)</span>
element.style.width = <span class="hljs-string">'100px'</span>;
element.style.height = <span class="hljs-string">'200px'</span>;
element.style.margin = <span class="hljs-string">'10px'</span>;
element.style.padding = <span class="hljs-string">'20px'</span>;
element.style.borderRadius = <span class="hljs-string">'5px'</span>;

<span class="hljs-comment">// Reinsert (causes one more reflow)</span>
<span class="hljs-keyword">if</span> (nextSibling) {
  parent.insertBefore(element, nextSibling);
} <span class="hljs-keyword">else</span> {
  parent.appendChild(element);
}
</code></pre>
<p><strong>Optimized version 5 – using display:none temporarily:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> element = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myElement'</span>);

<span class="hljs-comment">// Hide element (one reflow)</span>
element.style.display = <span class="hljs-string">'none'</span>;

<span class="hljs-comment">// Make multiple changes (no reflows while hidden)</span>
element.style.width = <span class="hljs-string">'100px'</span>;
element.style.height = <span class="hljs-string">'200px'</span>;
element.style.margin = <span class="hljs-string">'10px'</span>;
element.style.padding = <span class="hljs-string">'20px'</span>;
element.style.borderRadius = <span class="hljs-string">'5px'</span>;

<span class="hljs-comment">// Show element again (one more reflow)</span>
element.style.display = <span class="hljs-string">'block'</span>;
</code></pre>
<p>By using these optimization techniques, you can significantly reduce the number of reflows and repaints, leading to smoother performance, especially for animations and dynamic content updates.</p>
<h2 id="heading-12-use-event-delegation"><strong>12. Use Event Delegation</strong></h2>
<p>Event delegation takes advantage of event bubbling, allowing you to specify a single event handler to manage all events of a particular type. All events that use buttons (most mouse events and keyboard events) are suitable for the event delegation technique. Using event delegation can save memory.</p>
<pre><code class="lang-typescript">&lt;ul&gt;
  &lt;li&gt;Apple&lt;/li&gt;
  &lt;li&gt;Banana&lt;/li&gt;
  &lt;li&gt;Pineapple&lt;/li&gt;
&lt;/ul&gt;

<span class="hljs-comment">// good</span>
<span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'ul'</span>).onclick = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> target = event.target
  <span class="hljs-keyword">if</span> (target.nodeName === <span class="hljs-string">'LI'</span>) {
    <span class="hljs-built_in">console</span>.log(target.innerHTML)
  }
}

<span class="hljs-comment">// bad</span>
<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'li'</span>).forEach(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  e.onclick = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">this</span>.innerHTML)
  }
})
</code></pre>
<h2 id="heading-13-pay-attention-to-program-locality"><strong>13. Pay Attention to Program Locality</strong></h2>
<p>A well-written computer program often has good locality – it tends to reference data items near recently referenced data items or the recently referenced data items themselves. This tendency is known as the principle of locality. Programs with good locality run faster than those with poor locality.</p>
<h3 id="heading-locality-usually-takes-two-different-forms"><strong>Locality usually takes two different forms:</strong></h3>
<ul>
<li><p>Temporal locality: In a program with good temporal locality, memory locations that have been referenced once are likely to be referenced multiple times in the near future.</p>
</li>
<li><p>Spatial locality: In a program with good spatial locality, if a memory location has been referenced once, the program is likely to reference a nearby memory location in the near future.</p>
</li>
</ul>
<h4 id="heading-temporal-locality-example">Temporal locality example:</h4>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum</span>(<span class="hljs-params">arry</span>) </span>{
    <span class="hljs-keyword">let</span> i, sum = <span class="hljs-number">0</span>
    <span class="hljs-keyword">let</span> len = arry.length

    <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; len; i++) {
        sum += arry[i]
    }

    <span class="hljs-keyword">return</span> sum
}
</code></pre>
<p>In this example, the variable sum is referenced once in each loop iteration, so it has good temporal locality.</p>
<h4 id="heading-spatial-locality-example">Spatial locality example:</h4>
<p>Program with good spatial locality:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Two-dimensional array </span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum1</span>(<span class="hljs-params">arry, rows, cols</span>) </span>{
    <span class="hljs-keyword">let</span> i, j, sum = <span class="hljs-number">0</span>

    <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; rows; i++) {
        <span class="hljs-keyword">for</span> (j = <span class="hljs-number">0</span>; j &lt; cols; j++) {
            sum += arry[i][j]
        }
    }
    <span class="hljs-keyword">return</span> sum
}
</code></pre>
<p>Program with poor spatial locality:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Two-dimensional array </span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum2</span>(<span class="hljs-params">arry, rows, cols</span>) </span>{
    <span class="hljs-keyword">let</span> i, j, sum = <span class="hljs-number">0</span>

    <span class="hljs-keyword">for</span> (j = <span class="hljs-number">0</span>; j &lt; cols; j++) {
        <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; rows; i++) {
            sum += arry[i][j]
        }
    }
    <span class="hljs-keyword">return</span> sum
}
</code></pre>
<p>Looking at the two spatial locality examples above, the method of accessing each element of the array sequentially starting from each row, as shown in the examples, is called a reference pattern with a stride of 1.</p>
<p>If in an array, every k elements are accessed, it's called a reference pattern with a stride of k. Generally, as the stride increases, spatial locality decreases.</p>
<p>What's the difference between these two examples? Well, the first example scans the array by row, scanning one row completely before moving on to the next row. The second example scans the array by column, scanning one element in a row and immediately going to scan the same column element in the next row.</p>
<p>Arrays are stored in memory in row order, resulting in the example of scanning the array row by row getting a stride-1 reference pattern with good spatial locality. The other example has a stride of rows, with extremely poor spatial locality.</p>
<h3 id="heading-performance-testing"><strong>Performance Testing</strong></h3>
<p>Running environment:</p>
<ul>
<li><p>CPU: i5-7400</p>
</li>
<li><p>Browser: Chrome 70.0.3538.110</p>
</li>
</ul>
<p>Testing spatial locality on a two-dimensional array with a length of 9000 (child array length also 9000) 10 times, taking the average time (milliseconds), the results are as follows:</p>
<p>The examples used are the two spatial locality examples mentioned above.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Stride 1</td><td>Stride 9000</td></tr>
</thead>
<tbody>
<tr>
<td>124</td><td>2316</td></tr>
</tbody>
</table>
</div><p>From the test results above, the array with a stride of 1 executes an order of magnitude faster than the array with a stride of 9000.</p>
<p>So to sum up:</p>
<ul>
<li><p>Programs that repeatedly reference the same variables have good temporal locality</p>
</li>
<li><p>For programs with a reference pattern with a stride of k, the smaller the stride, the better the spatial locality; while programs that jump around in memory with large strides will have very poor spatial locality</p>
</li>
</ul>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://www.amazon.sg/Computer-Systems-Programmers-Perspective-3rd/dp/013409266X">Computer Systems: A Programmer's Perspective</a></li>
</ul>
<h2 id="heading-14-if-else-vs-switch"><strong>14. if-else vs switch</strong></h2>
<p>As the number of judgment conditions increases, it becomes more preferable to use switch instead of if-else.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (color == <span class="hljs-string">'blue'</span>) {

} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (color == <span class="hljs-string">'yellow'</span>) {

} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (color == <span class="hljs-string">'white'</span>) {

} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (color == <span class="hljs-string">'black'</span>) {

} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (color == <span class="hljs-string">'green'</span>) {

} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (color == <span class="hljs-string">'orange'</span>) {

} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (color == <span class="hljs-string">'pink'</span>) {

}

<span class="hljs-keyword">switch</span> (color) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'blue'</span>:

        <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'yellow'</span>:

        <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'white'</span>:

        <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'black'</span>:

        <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'green'</span>:

        <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'orange'</span>:

        <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'pink'</span>:

        <span class="hljs-keyword">break</span>
}
</code></pre>
<p>In situations like the one above, from a readability perspective, using switch is better (JavaScript's switch statement is not based on hash implementation but on loop judgment, so from a performance perspective, if-else and switch are the same).</p>
<h3 id="heading-why-switch-is-better-for-multiple-conditions"><strong>Why switch is better for multiple conditions:</strong></h3>
<ol>
<li><p><strong>Improved readability</strong>: Switch statements present a clearer visual structure when dealing with multiple conditions against the same variable. The case statements create a more organized, tabular format that's easier to scan and understand.</p>
</li>
<li><p><strong>Cleaner code maintenance</strong>: Adding or removing conditions in a switch statement is simpler and less error-prone. With if-else chains, it's easy to accidentally break the chain or forget an "else" keyword.</p>
</li>
<li><p><strong>Less repetition</strong>: In the if-else example, we repeat checking the same variable (<code>color</code>) multiple times, while in switch we specify it once at the top.</p>
</li>
<li><p><strong>Better for debugging</strong>: When debugging, it's easier to set breakpoints on specific cases in a switch statement than trying to identify which part of a long if-else chain you need to target.</p>
</li>
<li><p><strong>Intent signaling</strong>: Using switch communicates to other developers that you're checking multiple possible values of the same variable, rather than potentially unrelated conditions.</p>
</li>
</ol>
<p>For modern JavaScript, there's another alternative worth considering for simple value mapping – object literals:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> colorActions = {
  <span class="hljs-string">'blue'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* blue action */</span> },
  <span class="hljs-string">'yellow'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* yellow action */</span> },
  <span class="hljs-string">'white'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* white action */</span> },
  <span class="hljs-string">'black'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* black action */</span> },
  <span class="hljs-string">'green'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* green action */</span> },
  <span class="hljs-string">'orange'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* orange action */</span> },
  <span class="hljs-string">'pink'</span>: <span class="hljs-function">() =&gt;</span> { <span class="hljs-comment">/* pink action */</span> }
};

<span class="hljs-comment">// Execute the action if it exists</span>
<span class="hljs-keyword">if</span> (colorActions[color]) {
  colorActions[color]();
}
</code></pre>
<p>This approach provides even better performance (O(1) lookup time) compared to both if-else and switch statement approaches.</p>
<h2 id="heading-15-lookup-tables"><strong>15. Lookup Tables</strong></h2>
<p>When there are many conditional statements, using switch and if-else is not the best choice. In such cases, you might want to try lookup tables. Lookup tables can be constructed using arrays and objects.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">switch</span> (index) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'0'</span>:
        <span class="hljs-keyword">return</span> result0
    <span class="hljs-keyword">case</span> <span class="hljs-string">'1'</span>:
        <span class="hljs-keyword">return</span> result1
    <span class="hljs-keyword">case</span> <span class="hljs-string">'2'</span>:
        <span class="hljs-keyword">return</span> result2
    <span class="hljs-keyword">case</span> <span class="hljs-string">'3'</span>:
        <span class="hljs-keyword">return</span> result3
    <span class="hljs-keyword">case</span> <span class="hljs-string">'4'</span>:
        <span class="hljs-keyword">return</span> result4
    <span class="hljs-keyword">case</span> <span class="hljs-string">'5'</span>:
        <span class="hljs-keyword">return</span> result5
    <span class="hljs-keyword">case</span> <span class="hljs-string">'6'</span>:
        <span class="hljs-keyword">return</span> result6
    <span class="hljs-keyword">case</span> <span class="hljs-string">'7'</span>:
        <span class="hljs-keyword">return</span> result7
    <span class="hljs-keyword">case</span> <span class="hljs-string">'8'</span>:
        <span class="hljs-keyword">return</span> result8
    <span class="hljs-keyword">case</span> <span class="hljs-string">'9'</span>:
        <span class="hljs-keyword">return</span> result9
    <span class="hljs-keyword">case</span> <span class="hljs-string">'10'</span>:
        <span class="hljs-keyword">return</span> result10
    <span class="hljs-keyword">case</span> <span class="hljs-string">'11'</span>:
        <span class="hljs-keyword">return</span> result11
}
</code></pre>
<p>This switch statement can be converted to a lookup table:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]

<span class="hljs-keyword">return</span> results[index]
</code></pre>
<p>If the conditional statements are not numerical values but strings, you can use an object to build a lookup table:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> map = {
  red: result0,
  green: result1,
}

<span class="hljs-keyword">return</span> map[color]
</code></pre>
<h3 id="heading-why-lookup-tables-are-better-for-many-conditions"><strong>Why lookup tables are better for many conditions:</strong></h3>
<ol>
<li><p><strong>Constant time complexity (O(1))</strong>: Lookup tables provide direct access to the result based on the index/key, making the operation time constant regardless of how many options there are. In contrast, both if-else chains and switch statements have linear time complexity (O(n)) because in the worst case, they might need to check all conditions.</p>
</li>
<li><p><strong>Performance gains with many conditions</strong>: As the number of conditions increases, the performance advantage of lookup tables becomes more significant. For a small number of cases (2-5), the difference is negligible, but with dozens or hundreds of cases, lookup tables are substantially faster.</p>
</li>
<li><p><strong>Code brevity</strong>: As shown in the examples, lookup tables typically require less code, making your codebase more maintainable.</p>
</li>
<li><p><strong>Dynamic configuration</strong>: Lookup tables can be easily populated dynamically:</p>
</li>
</ol>
<pre><code class="lang-typescript">   <span class="hljs-keyword">const</span> actionMap = {};

   <span class="hljs-comment">// Dynamically populate the map</span>
   <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">registerAction</span>(<span class="hljs-params">key, handler</span>) </span>{
     actionMap[key] = handler;
   }

   <span class="hljs-comment">// Register different handlers</span>
   registerAction(<span class="hljs-string">'save'</span>, saveDocument);
   registerAction(<span class="hljs-string">'delete'</span>, deleteDocument);

   <span class="hljs-comment">// Use it</span>
   <span class="hljs-keyword">if</span> (actionMap[userAction]) {
     actionMap[userAction]();
   }
</code></pre>
<ol start="5">
<li><strong>Reduced cognitive load</strong>: When there are many conditions, lookup tables eliminate the mental overhead of following long chains of logic.</li>
</ol>
<h3 id="heading-when-to-use-each-approach"><strong>When to use each approach:</strong></h3>
<ul>
<li><p><strong>If-else</strong>: Best for a few conditions (2-3) with complex logic or different variables being checked</p>
</li>
<li><p><strong>Switch</strong>: Good for moderate number of conditions (4-10) checking against the same variable</p>
</li>
<li><p><strong>Lookup tables</strong>: Ideal for many conditions (10+) or when you need O(1) access time</p>
</li>
</ul>
<p>In real applications, lookup tables might be populated from external sources like databases or configuration files, making them flexible for scenarios where the mapping logic might change without requiring code modifications.</p>
<h2 id="heading-16-avoid-page-stuttering"><strong>16. Avoid Page Stuttering</strong></h2>
<h3 id="heading-60fps-and-device-refresh-rate"><strong>60fps and Device Refresh Rate</strong></h3>
<blockquote>
<p>Currently, most devices have a screen refresh rate of 60 times/second. Therefore, if there's an animation or gradient effect on the page, or if the user is scrolling the page, the browser needs to render animations or pages at a rate that matches the device's screen refresh rate.  </p>
<p>The budget time for each frame is just over 16 milliseconds (1 second / 60 = 16.66 milliseconds). But in reality, the browser has housekeeping work to do, so all your work needs to be completed within 10 milliseconds. If you can't meet this budget, the frame rate will drop, and content will jitter on the screen.  </p>
<p>This phenomenon is commonly known as stuttering and has a negative impact on user experience. <em>Source:</em> <a target="_blank" href="https://developers.google.com/web/fundamentals/performance/rendering"><em>Google Web Fundamentals - Rendering Performance</em></a></p>
</blockquote>
<p><a target="_blank" href="https://camo.githubusercontent.com/300b19e6e2523e1dfba3a8addba37a65797cc55de57501768ce987a81d06332f/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f31626565666137613665323039346465643966656261336165633832303135382e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp5pde3zqyadfrth7ypsf.png" alt="Frame budget timing diagram showing the 16ms frame budget and browser overhead" width="600" height="400" loading="lazy"></a></p>
<p>Suppose you use JavaScript to modify the DOM, trigger style changes, go through reflow and repaint, and finally paint to the screen. If any of these takes too long, it will cause the rendering time of this frame to be too long, and the average frame rate will drop. Suppose this frame took 50 ms, then the frame rate would be 1s / 50ms = 20fps, and the page would appear to stutter.</p>
<p>For some long-running JavaScript, we can use timers to split and delay execution.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>, len = arry.length; i &lt; len; i++) {
    process(arry[i])
}
</code></pre>
<p>Suppose the loop structure above takes too long due to either the high complexity of process() or too many array elements, or both, you might want to try splitting.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> todo = arry.concat()
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
    process(todo.shift())
    <span class="hljs-keyword">if</span> (todo.length) {
        <span class="hljs-built_in">setTimeout</span>(<span class="hljs-built_in">arguments</span>.callee, <span class="hljs-number">25</span>)
    } <span class="hljs-keyword">else</span> {
        callback(arry)
    }
}, <span class="hljs-number">25</span>)
</code></pre>
<p>If you're interested in learning more, check out <a target="_blank" href="https://www.amazon.com/High-Performance-JavaScript-Application-Interfaces/dp/059680279X">High Performance JavaScript</a> Chapter 6.</p>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://developers.google.com/web/fundamentals/performance/rendering">Rendering Performance</a></li>
</ul>
<h2 id="heading-17-use-requestanimationframe-to-implement-visual-changes"><strong>17. Use</strong> <code>requestAnimationFrame</code> <strong>to Implement Visual Changes</strong></h2>
<p>From point 16, we know that most devices have a screen refresh rate of 60 times/second, which means the average time per frame is 16.66 milliseconds. When using JavaScript to implement animation effects, the best case is that the code starts executing at the beginning of each frame. The only way to ensure JavaScript runs at the beginning of a frame is to use <code>requestAnimationFrame</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateScreen</span>(<span class="hljs-params">time</span>) </span>{
  <span class="hljs-comment">// Make visual updates here.</span>
}

requestAnimationFrame(updateScreen);
</code></pre>
<p>If you use <code>setTimeout</code> or <code>setInterval</code> to implement animations, the callback function will run at some point in the frame, possibly right at the end, which can often cause us to miss frames, leading to stuttering.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/6921c15237df7064a3fe41fa89a174d78b43a8a0764a3b7536051c59b223ef6d/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f32386238663463313066646333393633303135386562646162626264356432662e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F72qwmqhf5972jig808qz.png" alt="show the execution time of javascript" width="600" height="400" loading="lazy"></a></p>
<p>Reference:</p>
<ul>
<li><p><a target="_blank" href="https://web.dev/articles/optimize-javascript-execution">Optimize JavaScript Execution</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/immutable-javascript-improve-application-performance/">Improve JS performance</a></p>
</li>
</ul>
<h2 id="heading-18-use-web-workers"><strong>18. Use Web Workers</strong></h2>
<p>Web Workers use other worker threads to operate independently of the main thread. They can perform tasks without interfering with the user interface. A worker can send messages to the JavaScript code that created it by sending messages to the event handler specified by that code (and vice versa).</p>
<p>Web Workers are suitable for processing pure data or long-running scripts unrelated to the browser UI.</p>
<p>Creating a new worker is simple – just specify a script URI to execute the worker thread (main.js):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">var</span> myWorker = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">'worker.js'</span>);
<span class="hljs-comment">// You can send messages to the worker through the postMessage() method and onmessage event</span>
first.onchange = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
  myWorker.postMessage([first.value, second.value]);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Message posted to worker'</span>);
}

second.onchange = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
  myWorker.postMessage([first.value, second.value]);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Message posted to worker'</span>);
}
</code></pre>
<p>In the worker, after receiving the message, you can write an event handler function code as a response (worker.js):</p>
<pre><code class="lang-typescript">onmessage = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">e</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Message received from main script'</span>);
  <span class="hljs-keyword">var</span> workerResult = <span class="hljs-string">'Result: '</span> + (e.data[<span class="hljs-number">0</span>] * e.data[<span class="hljs-number">1</span>]);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Posting message back to main script'</span>);
  postMessage(workerResult);
}
</code></pre>
<p>The <code>onmessage</code> handler function executes immediately after receiving the message, and the message itself is used as the data property of the event. Here we simply multiply the two numbers and use the <code>postMessage()</code> method again to send the result back to the main thread.</p>
<p>Back in the main thread, we use <code>onmessage</code> again to respond to the message sent back from the worker:</p>
<pre><code class="lang-typescript">myWorker.onmessage = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">e</span>) </span>{
  result.textContent = e.data;
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Message received from worker'</span>);
}
</code></pre>
<p>Here we get the data from the message event and set it as the <code>textContent</code> of result, so the user can directly see the result of the calculation.</p>
<p>Note that inside the worker, you cannot directly manipulate DOM nodes, nor can you use the default methods and properties of the window object. But you can use many things under the window object, including data storage mechanisms such as WebSockets, IndexedDB, and Firefox OS-specific Data Store API.</p>
<p>Reference:</p>
<ul>
<li><p><a target="_blank" href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers">Web Workers</a></p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/how-webworkers-work-in-javascript-with-example/">How web workers work in JS</a></p>
</li>
</ul>
<h2 id="heading-19-use-bitwise-operations"><strong>19. Use Bitwise Operations</strong></h2>
<p>Numbers in JavaScript are stored in 64-bit format using the IEEE-754 standard. But in bitwise operations, numbers are converted to 32-bit signed format. Even with the conversion, bitwise operations are much faster than other mathematical and boolean operations.</p>
<h3 id="heading-modulo"><strong>Modulo</strong></h3>
<p>Since the lowest bit of even numbers is 0 and odd numbers is 1, modulo operations can be replaced with bitwise operations.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (value % <span class="hljs-number">2</span>) {
    <span class="hljs-comment">// Odd number</span>
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Even number </span>
}
<span class="hljs-comment">// Bitwise operation</span>
<span class="hljs-keyword">if</span> (value &amp; <span class="hljs-number">1</span>) {
    <span class="hljs-comment">// Odd number</span>
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Even number</span>
}
</code></pre>
<p><strong>How it works:</strong> The <code>&amp;</code> (bitwise AND) operator compares each bit of the first operand to the corresponding bit of the second operand. If both bits are 1, the corresponding result bit is set to 1; otherwise, it's set to 0.</p>
<p>When we do <code>value &amp; 1</code>, we're only checking the last bit of the number:</p>
<ul>
<li><p>For even numbers (for example, 4 = <code>100</code> in binary), the last bit is 0: <code>100 &amp; 001 = 000</code> (0)</p>
</li>
<li><p>For odd numbers (for example, 5 = <code>101</code> in binary), the last bit is 1: <code>101 &amp; 001 = 001</code> (1)</p>
</li>
</ul>
<h3 id="heading-floor"><strong>Floor</strong></h3>
<pre><code class="lang-typescript">~~<span class="hljs-number">10.12</span> <span class="hljs-comment">// 10</span>
~~<span class="hljs-number">10</span> <span class="hljs-comment">// 10</span>
~~<span class="hljs-string">'1.5'</span> <span class="hljs-comment">// 1</span>
~~<span class="hljs-literal">undefined</span> <span class="hljs-comment">// 0</span>
~~<span class="hljs-literal">null</span> <span class="hljs-comment">// 0</span>
</code></pre>
<p><strong>How it works:</strong> The <code>~</code> (bitwise NOT) operator inverts all bits in the operand. For a number <code>n</code>, <code>~n</code> equals <code>-(n+1)</code>. When applied twice (<code>~~n</code>), it effectively truncates the decimal part of a number, similar to <code>Math.floor()</code> for positive numbers and <code>Math.ceil()</code> for negative numbers.</p>
<p>The process:</p>
<ol>
<li><p>First <code>~</code>: Converts the number to a 32-bit integer and inverts all bits</p>
</li>
<li><p>Second <code>~</code>: Inverts all bits again, resulting in the original number but with decimal part removed</p>
</li>
</ol>
<p>For example:</p>
<pre><code class="lang-typescript">~<span class="hljs-number">10.12</span> → ~<span class="hljs-number">10</span> → -(<span class="hljs-number">10</span>+<span class="hljs-number">1</span>) → <span class="hljs-number">-11</span>
~(<span class="hljs-number">-11</span>) → -(<span class="hljs-number">-11</span>+<span class="hljs-number">1</span>) → -(<span class="hljs-number">-10</span>) → <span class="hljs-number">10</span>
</code></pre>
<h3 id="heading-bitmask"><strong>Bitmask</strong></h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> a = <span class="hljs-number">1</span>
<span class="hljs-keyword">const</span> b = <span class="hljs-number">2</span>
<span class="hljs-keyword">const</span> c = <span class="hljs-number">4</span>
<span class="hljs-keyword">const</span> options = a | b | c
</code></pre>
<p>By defining these options, you can use the bitwise AND operation to determine if a/b/c is in the options.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Is option b in the options?</span>
<span class="hljs-keyword">if</span> (b &amp; options) {
    ...
}
</code></pre>
<p><strong>How it works:</strong> In bitmasks, each bit represents a boolean flag. The values are typically powers of 2 so each has exactly one bit set.</p>
<ol>
<li><p><code>a = 1</code>: Binary <code>001</code></p>
</li>
<li><p><code>b = 2</code>: Binary <code>010</code></p>
</li>
<li><p><code>c = 4</code>: Binary <code>100</code></p>
</li>
<li><p><code>options = a | b | c</code>: The <code>|</code> (bitwise OR) combines them: <code>001 | 010 | 100 = 111</code> (binary) = 7 (decimal)</p>
</li>
</ol>
<p>When checking if a flag is set with <code>if (b &amp; options)</code>:</p>
<ul>
<li><p><code>b &amp; options</code> = <code>010 &amp; 111</code> = <code>010</code> = 2 (decimal)</p>
</li>
<li><p>Since this is non-zero, the condition evaluates to true</p>
</li>
</ul>
<p>This technique is extremely efficient for storing and checking multiple boolean values in a single number, and is commonly used in systems programming, graphics programming, and permission systems.</p>
<h2 id="heading-20-dont-override-native-methods"><strong>20. Don't Override Native Methods</strong></h2>
<p>No matter how optimized your JavaScript code is, it can't match native methods. This is because native methods are written in low-level languages (C/C++) and compiled into machine code, becoming part of the browser. When native methods are available, try to use them, especially for mathematical operations and DOM manipulations.</p>
<h3 id="heading-example-string-replacement-native-vs-custom"><strong>Example: String Replacement (Native vs. Custom)</strong></h3>
<p>A common pitfall is rewriting native string methods like <code>replaceAll()</code>. Below is an inefficient custom implementation versus the native method, with performance benchmarks:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Inefficient custom global replacement (manual loop)  </span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">customReplaceAll</span>(<span class="hljs-params">str, oldSubstr, newSubstr</span>) </span>{  
  <span class="hljs-keyword">let</span> result = <span class="hljs-string">''</span>;  
  <span class="hljs-keyword">let</span> index = <span class="hljs-number">0</span>;  
  <span class="hljs-keyword">while</span> (index &lt; str.length) {  
    <span class="hljs-keyword">if</span> (str.slice(index, index + oldSubstr.length) === oldSubstr) {  
      result += newSubstr;  
      index += oldSubstr.length;  
    } <span class="hljs-keyword">else</span> {  
      result += str[index];  
      index++;  
    }  
  }  
  <span class="hljs-keyword">return</span> result;  
}  

<span class="hljs-comment">// Efficient native method (browser-optimized)  </span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">nativeReplaceAll</span>(<span class="hljs-params">str, oldSubstr, newSubstr</span>) </span>{  
  <span class="hljs-keyword">return</span> str.replaceAll(oldSubstr, newSubstr);  
}  

<span class="hljs-comment">// Test with a large string (100,000 repetitions of "abc ")  </span>
<span class="hljs-keyword">const</span> largeString = <span class="hljs-string">'abc '</span>.repeat(<span class="hljs-number">100000</span>);  

<span class="hljs-comment">// Benchmark: Custom implementation  </span>
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'customReplaceAll'</span>);  
customReplaceAll(largeString, <span class="hljs-string">'abc'</span>, <span class="hljs-string">'xyz'</span>);  
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'customReplaceAll'</span>); <span class="hljs-comment">// Output: ~5ms (varies by browser)  </span>

<span class="hljs-comment">// Benchmark: Native method  </span>
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'nativeReplaceAll'</span>);  
nativeReplaceAll(largeString, <span class="hljs-string">'abc'</span>, <span class="hljs-string">'xyz'</span>);  
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'nativeReplaceAll'</span>); <span class="hljs-comment">// Output: ~2ms (typically 2-3x faster)</span>
</code></pre>
<p>Key takeaways:</p>
<ul>
<li><p><strong>Performance</strong>: Native methods like <code>replaceAll()</code> are optimized at the browser level, often outperforming handwritten code (as shown in the benchmark above).</p>
</li>
<li><p><strong>Maintainability</strong>: Native methods are standardized, well-documented, and less error-prone than custom logic (for example, handling edge cases like overlapping substrings).</p>
</li>
<li><p><strong>Ecosystem compatibility</strong>: Using native methods ensures consistency with libraries and tools that rely on JavaScript’s built-in behavior.</p>
</li>
</ul>
<h3 id="heading-when-to-use-custom-code"><strong>When to Use Custom Code</strong></h3>
<p>While native methods are usually superior, there are rare cases where you might need custom logic:</p>
<ul>
<li><p>When the native method doesn’t exist (for example, polyfilling for older browsers).</p>
</li>
<li><p>For highly specialized edge cases not covered by native APIs.</p>
</li>
<li><p>When you need to avoid function call overhead in extremely performance-critical loops (for example, tight numerical computations).</p>
</li>
</ul>
<p><strong>Remember</strong>: Browser vendors spend millions of hours optimizing native methods. By leveraging them, you gain free performance boosts and reduce the risk of reinventing flawed solutions.</p>
<h2 id="heading-21-reduce-the-complexity-of-css-selectors"><strong>21. Reduce the Complexity of CSS Selectors</strong></h2>
<h3 id="heading-1-when-browsers-read-selectors-they-follow-the-principle-of-reading-from-right-to-left"><strong>1. When browsers read selectors, they follow the principle of reading from right to left.</strong></h3>
<p>Let's look at an example:</p>
<pre><code class="lang-typescript">#block .text p {
    color: red;
}
</code></pre>
<ol>
<li><p>Find all P elements.</p>
</li>
<li><p>Check if the elements found in result 1 have parent elements with class name "text"</p>
</li>
<li><p>Check if the elements found in result 2 have parent elements with ID "block"</p>
</li>
</ol>
<p><strong>Why is this inefficient?</strong> This right-to-left evaluation process can be very expensive in complex documents. Take the selector <code>#block .text p</code> as an example:</p>
<ol>
<li><p>The browser first finds <strong>all</strong> <code>p</code> elements in the document (potentially hundreds)</p>
</li>
<li><p>For each of those paragraph elements, it must check if any of their ancestors have the class <code>text</code></p>
</li>
<li><p>For those that pass step 2, it must check if any of their ancestors have the ID <code>block</code></p>
</li>
</ol>
<p>This creates a significant performance bottleneck because:</p>
<ul>
<li><p>The initial selection (<code>p</code>) is very broad</p>
</li>
<li><p>Each subsequent step requires checking multiple ancestors in the DOM tree</p>
</li>
<li><p>This process repeats for every paragraph element</p>
</li>
</ul>
<p>A more efficient alternative would be:</p>
<pre><code class="lang-typescript">#block p.specific-text {
    color: red;
}
</code></pre>
<p>This is more efficient because it directly targets only paragraphs with a specific class, avoiding checking all paragraphs</p>
<h3 id="heading-2-css-selector-priority"><strong>2. CSS selector priority</strong></h3>
<pre><code class="lang-typescript">Inline &gt; ID selector &gt; Class selector &gt; Tag selector
</code></pre>
<p>Based on the above two pieces of information, we can draw conclusions:</p>
<ol>
<li><p>The shorter the selector, the better.</p>
</li>
<li><p>Try to use high-priority selectors, such as ID and class selectors.</p>
</li>
<li><p>Avoid using the universal selector *.</p>
</li>
</ol>
<p><strong>Practical advice for optimal CSS selectors:</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">/* ❌ Inefficient: Too deep, starts with a tag selector */</span>
body div.container ul li a.link {
    color: blue;
}

<span class="hljs-comment">/* ✅ Better: Shorter, starts with a class selector */</span>
.container .link {
    color: blue;
}

<span class="hljs-comment">/* ✅ Best: Direct, single class selector */</span>
.nav-link {
    color: blue;
}
</code></pre>
<p>Finally, I should say that according to the materials I've found, there's no need to optimize CSS selectors because the performance difference between the slowest and fastest selectors is very small.</p>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://www.sitepoint.com/optimizing-css-id-selectors-and-other-myths/">Optimizing CSS: ID Selectors and Other Myths</a></li>
</ul>
<h2 id="heading-22-use-flexbox-instead-of-earlier-layout-models"><strong>22. Use Flexbox Instead of Earlier Layout Models</strong></h2>
<p>In early CSS layout methods, we could position elements absolutely, relatively, or using floats. Now, we have a new layout method called <a target="_blank" href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox">Flexbox</a>, which has an advantage over earlier layout methods: better performance.</p>
<p>The screenshot below shows the layout cost of using floats on 1300 boxes:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/ff6a96a175ccd6a4a55e0a0ea2932833cae4f639ddfda73c330f056eb2311efa/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f37343264613262643539656537613331396239363036643461393539323234392e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwsgqx7lcp8q0bizrtmb.png" alt="layout timeline in dev tool" width="600" height="400" loading="lazy"></a></p>
<p>Then we recreate this example using Flexbox:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/18ad08d69431cc0ef0d60b3aa748aa1e0220329cb6043046eeb744ad3ec64abe/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f63633831663131613634643232613863656334643935616638633136376537362e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9pmmihyz1ql8761ae2k.png" alt="layout timeline in dev tool" width="600" height="400" loading="lazy"></a></p>
<p>Now, for the same number of elements and the same visual appearance, the layout time is much less (3.5 milliseconds versus 14 milliseconds in this example).</p>
<p>But Flexbox compatibility is still an issue, as not all browsers support it, so use it with caution.</p>
<p>Browser compatibility:</p>
<ul>
<li><p>Chrome 29+</p>
</li>
<li><p>Firefox 28+</p>
</li>
<li><p>Internet Explorer 11</p>
</li>
<li><p>Opera 17+</p>
</li>
<li><p>Safari 6.1+ (prefixed with -webkit-)</p>
</li>
<li><p>Android 4.4+</p>
</li>
<li><p>iOS 7.1+ (prefixed with -webkit-)</p>
</li>
</ul>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing">Use flexbox instead of earlier layout models</a></li>
</ul>
<h2 id="heading-23-use-transform-and-opacity-properties-to-implement-animations"><strong>23. Use Transform and Opacity Properties to Implement Animations</strong></h2>
<p>In CSS, transforms and opacity property changes don't trigger reflow and repaint. They’re properties that can be processed by the compositor alone.</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/00e5e11d0b2837e91e8118284520b5969ae69670c8607d791e2053599fee0b4e/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f696d675f636f6e766572742f66626436333931363533376336623531373733633266623134343263663130632e706e67"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ae64ihgp1781wrtfci8.png" alt="Diagram showing how transform and opacity properties bypass layout and paint processes" width="600" height="400" loading="lazy"></a></p>
<h3 id="heading-example-inefficient-vs-efficient-animation"><strong>Example: Inefficient vs. Efficient Animation</strong></h3>
<p>❌ Inefficient animation using properties that trigger reflow and repaint:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/* CSS */</span>
.box-inefficient {
  position: absolute;
  left: <span class="hljs-number">0</span>;
  top: <span class="hljs-number">0</span>;
  width: <span class="hljs-number">100</span>px;
  height: <span class="hljs-number">100</span>px;
  background-color: #<span class="hljs-number">3498</span>db;
  animation: move-inefficient <span class="hljs-number">2</span>s infinite alternate;
}

<span class="hljs-meta">@keyframes</span> move-inefficient {
  to {
    left: <span class="hljs-number">300</span>px;
    top: <span class="hljs-number">200</span>px;
    width: <span class="hljs-number">150</span>px;
    height: <span class="hljs-number">150</span>px;
  }
}
</code></pre>
<p>This animation constantly triggers layout recalculations (reflow) because it animates position (<code>left</code>/<code>top</code>) and size (<code>width</code>/<code>height</code>) properties.</p>
<p>✅ Efficient animation using transform and opacity:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">/* CSS */</span>
.box-efficient {
  position: absolute;
  width: <span class="hljs-number">100</span>px;
  height: <span class="hljs-number">100</span>px;
  background-color: #<span class="hljs-number">3498</span>db;
  animation: move-efficient <span class="hljs-number">2</span>s infinite alternate;
}

<span class="hljs-meta">@keyframes</span> move-efficient {
  to {
    transform: translate(<span class="hljs-number">300</span>px, <span class="hljs-number">200</span>px) scale(<span class="hljs-number">1.5</span>);
    opacity: <span class="hljs-number">0.7</span>;
  }
}
</code></pre>
<p><strong>Why this is better:</strong></p>
<ol>
<li><p><code>transform: translate(300px, 200px)</code> replaces <code>left: 300px; top: 200px</code></p>
</li>
<li><p><code>transform: scale(1.5)</code> replaces <code>width: 150px; height: 150px</code></p>
</li>
<li><p>These transform operations and opacity changes can be handled directly by the GPU without triggering layout or paint operations</p>
</li>
</ol>
<p><strong>Performance comparison:</strong></p>
<ol>
<li><p>The inefficient version may drop frames on lower-end devices because each frame requires:</p>
<ul>
<li>JavaScript → Style calculations → Layout → Paint → Composite</li>
</ul>
</li>
<li><p>The efficient version typically maintains 60fps because it only requires:</p>
<ul>
<li>JavaScript → Style calculations → Composite</li>
</ul>
</li>
</ol>
<p><strong>HTML implementation:</strong></p>
<pre><code class="lang-typescript">&lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"box-inefficient"</span>&gt;Inefficient&lt;/div&gt;
&lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"box-efficient"</span>&gt;Efficient&lt;/div&gt;
</code></pre>
<p>For complex animations, you can use the Chrome DevTools Performance panel to visualize the difference. The inefficient animation will show many more layout and paint events compared to the efficient one.</p>
<p>Reference:</p>
<ul>
<li><a target="_blank" href="https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count">Use transform and opacity property changes to implement animations</a></li>
</ul>
<h2 id="heading-24-use-rules-reasonably-avoid-over-optimization"><strong>24. Use Rules Reasonably, Avoid Over-Optimization</strong></h2>
<p>Performance optimization is mainly divided into two categories:</p>
<ol>
<li><p>Load-time optimization</p>
</li>
<li><p>Runtime optimization</p>
</li>
</ol>
<p>Of the 23 suggestions above, the first 10 belong to load-time optimization, and the last 13 belong to runtime optimization. Usually, there's no need to apply all 23 performance optimization rules. It's best to make targeted adjustments based on the website's user group, saving effort and time.</p>
<p>Before solving a problem, you need to identify the problem first, otherwise you won't know where to start. So before doing performance optimization, it's best to investigate the website's loading and running performance.</p>
<h3 id="heading-check-loading-performance"><strong>Check Loading Performance</strong></h3>
<p>A website's loading performance mainly depends on white screen time and first screen time.</p>
<ul>
<li><p>White screen time: The time from entering the URL to when the page starts displaying content.</p>
</li>
<li><p>First screen time: The time from entering the URL to when the page is completely rendered.</p>
</li>
</ul>
<p>You can get the white screen time by placing the following script before <code>&lt;/head&gt;</code>.</p>
<pre><code class="lang-typescript">&lt;script&gt;
  <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() - performance.timing.navigationStart
  <span class="hljs-comment">// You can also use domLoading and navigationStart</span>
  performance.timing.domLoading - performance.timing.navigationStart
&lt;/script&gt;
</code></pre>
<p>You can get the first screen time by executing <code>new Date() - performance.timing.navigationStart</code> in the <code>window.onload</code> event.</p>
<h3 id="heading-check-runtime-performance"><strong>Check Runtime Performance</strong></h3>
<p>With Chrome's developer tools, we can check the website's performance during runtime.</p>
<p>Open the website, press F12 and select performance, click the gray dot in the upper left corner, it turns red to indicate it has started recording. At this point, you can simulate users using the website, and after you're done, click stop, then you'll see the website's performance report during the runtime.</p>
<p>If there are red blocks, it means there are frame drops. If it's green, it means the FPS is good. For detailed usage of performance, you can search using a search engine, as the scope is limited.</p>
<p>By checking the loading and runtime performance, I believe you already have a general understanding of the website's performance. So what you need to do now is to use the 23 suggestions above to optimize your website. Go for it!</p>
<p>References:</p>
<ul>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming/navigationStart">performance.timing.navigationStart</a></li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Performance optimization is a critical aspect of modern web development that directly impacts user experience, engagement, and ultimately, business outcomes. Throughout this article, we've explored 24 diverse techniques spanning various layers of web applications – from network optimization to rendering performance and JavaScript execution.</p>
<h3 id="heading-key-takeaways"><strong>Key Takeaways</strong></h3>
<ol>
<li><p><strong>Start with measurement, not optimization</strong>. As discussed in point #24, always identify your specific performance bottlenecks before applying optimization techniques. Tools like Chrome DevTools Performance panel, Lighthouse, and WebPageTest can help pinpoint exactly where your application is struggling.</p>
</li>
<li><p><strong>Focus on the critical rendering path</strong>. Many of our techniques (placing CSS in the head, JavaScript at the bottom, reducing HTTP requests, server-side rendering) are centered around speeding up the time to first meaningful paint – the moment when users see and can interact with your content.</p>
</li>
<li><p><strong>Understand the browser rendering process</strong>. Knowledge of how browsers parse HTML, execute JavaScript, and render pixels to the screen is essential for making informed optimization decisions, especially when dealing with animations and dynamic content.</p>
</li>
<li><p><strong>Balance implementation cost vs. performance gain</strong>. Not all optimization techniques are worth implementing for every project. For instance, server-side rendering adds complexity that might not be justified for simple applications, and bitwise operations provide performance gains only in specific heavy computation scenarios.</p>
</li>
<li><p><strong>Consider the device and network conditions of your users</strong>. If you're building for users in regions with slower internet connections or less powerful devices, techniques like image optimization, code splitting, and reducing JavaScript payloads become even more important.</p>
</li>
</ol>
<h3 id="heading-practical-implementation-strategy"><strong>Practical Implementation Strategy</strong></h3>
<p>Instead of trying to implement all 24 techniques at once, consider taking a phased approach:</p>
<ol>
<li><p><strong>First pass</strong>: Implement the easy wins with high impact</p>
<ul>
<li><p>Proper image optimization</p>
</li>
<li><p>HTTP/2</p>
</li>
<li><p>Basic caching</p>
</li>
<li><p>CSS/JS placement</p>
</li>
</ul>
</li>
<li><p><strong>Second pass</strong>: Address specific measured bottlenecks</p>
<ul>
<li><p>Use performance profiling to identify problem areas</p>
</li>
<li><p>Apply targeted optimizations based on findings</p>
</li>
</ul>
</li>
<li><p><strong>Ongoing maintenance</strong>: Make performance part of your development workflow</p>
<ul>
<li><p>Set performance budgets</p>
</li>
<li><p>Implement automated performance testing</p>
</li>
<li><p>Review new feature additions for performance impact</p>
</li>
</ul>
</li>
</ol>
<p>By treating performance as an essential feature rather than an afterthought, you'll create web applications that not only look good and function well but also provide the speed and responsiveness that modern users expect.</p>
<p>Remember that web performance is a continuous journey, not a destination. Browsers evolve, best practices change, and user expectations increase. The techniques in this article provide a strong foundation, but staying current with web performance trends will ensure your applications remain fast and effective for years to come.</p>
<h3 id="heading-other-references"><strong>Other References</strong></h3>
<ul>
<li><p><a target="_blank" href="https://web.dev/learn/performance/why-speed-matters">Why Performance Matters</a></p>
</li>
<li><p><a target="_blank" href="https://www.amazon.com/High-Performance-Web-Sites-Essential/dp/0596529309">High-Performance Website Construction Guide</a></p>
</li>
<li><p><a target="_blank" href="https://hpbn.co/">High Performance Browser Networking</a></p>
</li>
<li><p><a target="_blank" href="https://www.amazon.com/High-Performance-JavaScript-Application-Interfaces/dp/059680279X">High-Performance JavaScript</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Write Unit Tests and E2E Tests for NestJS Applications ]]>
                </title>
                <description>
                    <![CDATA[ Recently, I have been writing unit tests and E2E tests for a NestJS project. This was my first time writing tests for a backend project, and I found the process different from my experience with frontend testing, making it challenging to begin. After... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/nestjs-unit-testing-e2e-testing-guide/</link>
                <guid isPermaLink="false">67ffc1695667d9e59ef9bc46</guid>
                
                    <category>
                        <![CDATA[ nestjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ E2ETesting ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gordan Tan ]]>
                </dc:creator>
                <pubDate>Wed, 16 Apr 2025 14:40:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744738441654/1bb2b329-d363-46d7-b091-e0e95ad22c9e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Recently, I have been writing unit tests and E2E tests for a NestJS project. This was my first time writing tests for a backend project, and I found the process different from my experience with frontend testing, making it challenging to begin.</p>
<p>After looking at some examples, I have gained a clearer understanding of how to approach testing. So I wrote an article to record and share my learning to help others who may be facing similar confusion.</p>
<p>In addition, I have put together a demo project with the relevant unit and E2E tests completed, which may be of interest. I’ve <a target="_blank" href="https://github.com/woai3c/nestjs-demo">uploaded the code to Github here</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-difference-between-unit-testing-and-e2e-testing">Difference Between Unit Testing and E2E Testing</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-writing-unit-tests">Writing Unit Tests</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-the-first-test-case">The First Test Case</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-second-test-case">The Second Test Case</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-unit-test-coverage">Unit Test Coverage</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-writing-e2e-tests">Writing E2E Tests</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-whether-to-write-tests">Whether to Write Tests</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-enhancing-system-robustness">Enhancing System Robustness</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-enhancing-maintainability">Enhancing Maintainability</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-enhancing-development-efficiency">Enhancing Development Efficiency</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-when-not-to-write-tests">When Not to Write Tests?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reference-materials">Reference Materials</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving into this tutorial, you should have:</p>
<ul>
<li><p>Basic knowledge of TypeScript and Node.js</p>
</li>
<li><p>Familiarity with NestJS fundamentals</p>
</li>
<li><p>Understanding of RESTful APIs</p>
</li>
<li><p>MongoDB installed (as the example uses MongoDB)</p>
</li>
<li><p>Node.js and npm/yarn installed on your system</p>
</li>
<li><p>Basic understanding of testing concepts</p>
</li>
</ul>
<p>You can find the complete code examples in the <a target="_blank" href="https://github.com/woai3c/nestjs-demo">demo repository</a>. You can clone it to follow along with the examples.</p>
<h2 id="heading-difference-between-unit-testing-and-e2e-testing">Difference Between Unit Testing and E2E Testing</h2>
<p>Unit tests and E2E tests are methods of software testing, but they have different goals and scopes.</p>
<p>Unit testing involves checking and verifying the smallest testable unit within the software. A function or a method, for example, can be considered a unit. In unit testing, you provide expected outputs for various inputs of a function and validate the correctness of its operation. The goal of unit testing is to quickly identify bugs within the function, and they are easy to write and execute rapidly.</p>
<p>On the other hand, E2E tests often simulate real-world user scenarios to test the entire application. For instance, the frontend typically uses a browser or headless browser for testing, while the backend does so by simulating API calls.</p>
<p>Within a NestJS project, unit tests might assess a specific service or a method of a controller, such as verifying if the <code>update</code> method in the Users module correctly updates a user. An E2E test, however, may examine a complete user journey, from creating a new user to updating their password and eventually deleting the user, which involves multiple services and controllers.</p>
<h2 id="heading-how-to-write-unit-tests">How to Write Unit Tests</h2>
<p>Writing unit tests for a utility function or method that doesn't involve interfaces is relatively straightforward. You only need to consider the various inputs and write the corresponding test code. But the situation becomes more complex once interfaces come into play. Let's use code as an example:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> validateUser(
  username: <span class="hljs-built_in">string</span>,
  password: <span class="hljs-built_in">string</span>,
): <span class="hljs-built_in">Promise</span>&lt;UserAccountDto&gt; {
  <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.findOne({ username });
  <span class="hljs-keyword">if</span> (!entity) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'User not found'</span>);
  }
  <span class="hljs-keyword">if</span> (entity.lockUntil &amp;&amp; entity.lockUntil &gt; <span class="hljs-built_in">Date</span>.now()) {
    <span class="hljs-keyword">const</span> diffInSeconds = <span class="hljs-built_in">Math</span>.round((entity.lockUntil - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>);
    <span class="hljs-keyword">let</span> message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInSeconds}</span> seconds.`</span>;
    <span class="hljs-keyword">if</span> (diffInSeconds &gt; <span class="hljs-number">60</span>) {
      <span class="hljs-keyword">const</span> diffInMinutes = <span class="hljs-built_in">Math</span>.round(diffInSeconds / <span class="hljs-number">60</span>);
      message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInMinutes}</span> minutes.`</span>;
    }
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(message);
  }
  <span class="hljs-keyword">const</span> passwordMatch = bcrypt.compareSync(password, entity.password);
  <span class="hljs-keyword">if</span> (!passwordMatch) {
    <span class="hljs-comment">// $inc update to increase failedLoginAttempts</span>
    <span class="hljs-keyword">const</span> update = {
      $inc: { failedLoginAttempts: <span class="hljs-number">1</span> },
    };
    <span class="hljs-comment">// lock account when the third try is failed</span>
    <span class="hljs-keyword">if</span> (entity.failedLoginAttempts + <span class="hljs-number">1</span> &gt;= <span class="hljs-number">3</span>) {
      <span class="hljs-comment">// $set update to lock the account for 5 minutes</span>
      update[<span class="hljs-string">'$set'</span>] = { lockUntil: <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">5</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span> };
    }
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.update(entity._id, update);
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'Invalid password'</span>);
  }
  <span class="hljs-comment">// if validation is sucessful, then reset failedLoginAttempts and lockUntil</span>
  <span class="hljs-keyword">if</span> (
    entity.failedLoginAttempts &gt; <span class="hljs-number">0</span> ||
    (entity.lockUntil &amp;&amp; entity.lockUntil &gt; <span class="hljs-built_in">Date</span>.now())
  ) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.update(entity._id, {
      $set: { failedLoginAttempts: <span class="hljs-number">0</span>, lockUntil: <span class="hljs-literal">null</span> },
    });
  }
  <span class="hljs-keyword">return</span> { userId: entity._id, username } <span class="hljs-keyword">as</span> UserAccountDto;
}
</code></pre>
<p>The code above is a method <code>validateUser</code> in the <code>auth.service.ts</code> file, primarily used to verify whether the username and password entered by the user during login are correct. It contains the following logic:</p>
<ol>
<li><p>Check if the user exists based on <code>username</code>. If not, throw a 401 exception (a 404 exception is also feasible).</p>
</li>
<li><p>See if the user is locked out. If so, throw a 401 exception with a relevant message.</p>
</li>
<li><p>Encrypt the <code>password</code> and compare it with the password in the database. If it's incorrect, throw a 401 exception (three consecutive failed login attempts will lock the account for 5 minutes).</p>
</li>
<li><p>If the login is successful, clear any previously failed login attempt counts (if applicable) and return the user <code>id</code> and <code>username</code> to the next stage.</p>
</li>
</ol>
<p>As you can see, the <code>validateUser</code> method includes four processing logics. So we need to write corresponding unit test code for these four points to ensure that the entire <code>validateUser</code> function is operating correctly.</p>
<h3 id="heading-the-first-test-case">The First Test Case</h3>
<p>When we start writing unit tests, we encounter a problem: the <code>findOne</code> method needs to interact with the database, and it looks for corresponding users in the database through <code>username</code>. But if every unit test has to interact with the database, the testing will become very cumbersome. So we can mock fake data to achieve this.</p>
<p>For example, assume we have registered a user named <code>woai3c</code>. Then, during login, the user data can be retrieved in the <code>validateUser</code> method through <code>const entity = await this.usersService.findOne({ username });</code>. As long as this line of code can return the desired data, there is no problem, even without database interaction. We can achieve this through mock data.</p>
<p>Now, let's look at the relevant test code for the <code>validateUser</code> method:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Test } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/testing'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/modules/auth/auth.service'</span>;
<span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/modules/users/users.service'</span>;
<span class="hljs-keyword">import</span> { UnauthorizedException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { TEST_USER_NAME, TEST_USER_PASSWORD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tests/constants'</span>;
describe(<span class="hljs-string">'AuthService'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">let</span> authService: AuthService; <span class="hljs-comment">// Use the actual AuthService type</span>
  <span class="hljs-keyword">let</span> usersService: Partial&lt;Record&lt;keyof UsersService, jest.Mock&gt;&gt;;
  beforeEach(<span class="hljs-keyword">async</span> () =&gt; {
    usersService = {
      findOne: jest.fn(),
    };
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">module</span> = await Test.createTestingModule({
      providers: [        AuthService,
        {
          provide: UsersService,
          useValue: usersService,
        },
      ],
    }).compile();
    authService = <span class="hljs-built_in">module</span>.get&lt;AuthService&gt;(AuthService);
  });
  describe(<span class="hljs-string">'validateUser'</span>, <span class="hljs-function">() =&gt;</span> {
    it(<span class="hljs-string">'should throw an UnauthorizedException if user is not found'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">await</span> expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });
    <span class="hljs-comment">// other tests...</span>
  });
});
</code></pre>
<p>We get the user data by calling the <code>findOne</code> method of <code>usersService</code>, so we need to mock the <code>findOne</code> method of <code>usersService</code> in the test code:</p>
<pre><code class="lang-typescript">beforeEach(<span class="hljs-keyword">async</span> () =&gt; {
    usersService = {
      findOne: jest.fn(), <span class="hljs-comment">// mock findOne method</span>
    };
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">module</span> = await Test.createTestingModule({
      providers: [        AuthService, <span class="hljs-comment">// real AuthService, because we are testing its methods</span>
        {
          provide: UsersService, <span class="hljs-comment">// use mock usersService instead of real usersService</span>
          useValue: usersService,
        },
      ],
    }).compile();
    authService = <span class="hljs-built_in">module</span>.get&lt;AuthService&gt;(AuthService);
  });
</code></pre>
<p>We use <code>jest.fn()</code> to return a function to replace the real <code>usersService.findOne()</code>. If <code>usersService.findOne()</code> is called now, there will be no return value, so the first unit test case will pass:</p>
<pre><code class="lang-typescript">it(<span class="hljs-string">'should throw an UnauthorizedException if user is not found'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});
</code></pre>
<p>Since <code>findOne</code> in <code>const entity = await this.usersService.findOne({ username });</code> of the <code>validateUser</code> method is a mocked fake function with no return value, the 2nd to 4th lines of code in the <code>validateUser</code> method could execute:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (!entity) {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'User not found'</span>);
}
</code></pre>
<p>It throws a 401 error, which is as expected.</p>
<h3 id="heading-the-second-test-case">The Second Test Case</h3>
<p>The second logic in the <code>validateUser</code> method is to determine if the user is locked, with the corresponding code as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (entity.lockUntil &amp;&amp; entity.lockUntil &gt; <span class="hljs-built_in">Date</span>.now()) {
  <span class="hljs-keyword">const</span> diffInSeconds = <span class="hljs-built_in">Math</span>.round((entity.lockUntil - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>);
  <span class="hljs-keyword">let</span> message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInSeconds}</span> seconds.`</span>;
  <span class="hljs-keyword">if</span> (diffInSeconds &gt; <span class="hljs-number">60</span>) {
    <span class="hljs-keyword">const</span> diffInMinutes = <span class="hljs-built_in">Math</span>.round(diffInSeconds / <span class="hljs-number">60</span>);
    message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInMinutes}</span> minutes.`</span>;
  }
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(message);
}
</code></pre>
<p>As you can see, we can determine that the current account is locked if there is a lock time <code>lockUntil</code> in the user data and the lock end time is greater than the current time. So we need to mock a user data with the <code>lockUntil</code> field:</p>
<pre><code class="lang-typescript">it(<span class="hljs-string">'should throw an UnauthorizedException if the account is locked'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> lockedUser = {
    _id: TEST_USER_ID,
    username: TEST_USER_NAME,
    password: TEST_USER_PASSWORD,
    lockUntil: <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">5</span>, <span class="hljs-comment">// The account is locked for 5 minutes</span>
  };
  usersService.findOne.mockResolvedValueOnce(lockedUser);
  <span class="hljs-keyword">await</span> expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});
</code></pre>
<p>In the test code above, an object <code>lockedUser</code> is first defined, which contains the <code>lockUntil</code> field we need. Then, it is used as the return value for <code>findOne</code>, achieved by <code>usersService.findOne.mockResolvedValueOnce(lockedUser);</code>. Thus, when the <code>validateUser</code> method is executed, the user data within it is the mocked data, successfully allowing the second test case to pass.</p>
<h3 id="heading-unit-test-coverage">Unit Test Coverage</h3>
<p>Unit test coverage (Code Coverage) is a metric used to describe how much of the application code has been covered or tested by unit tests. It is typically expressed as a percentage, indicating how much of all possible code paths have been covered by test cases.</p>
<p>Unit test coverage usually includes the following types:</p>
<ul>
<li><p>Line Coverage: How many lines of code are covered by the tests.</p>
</li>
<li><p>Function Coverage: How many functions or methods are covered by the tests.</p>
</li>
<li><p>Branch Coverage: How many code branches are covered by the tests (for example, <code>if/else</code> statements).</p>
</li>
<li><p>Statement Coverage: How many statements in the code are covered by the tests.</p>
</li>
</ul>
<p>Unit test coverage is an important metric to measure the quality of unit tests, but it is not the only metric. A high coverage rate can help to detect errors in the code, but it does not guarantee the quality of the code. A low coverage rate may mean that there is untested code, potentially with undetected errors.</p>
<p>The image below shows the unit test coverage results for a demo project:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/e3de4ecc6be093ac92a514fa183f688c455b00cc15a3d003bfe2f25e31a08c4f/68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f76322f726573697a653a6669743a313331302f666f726d61743a776562702f302a515a5f4d4a77774c715752314d3136652e706e67"><img src="https://camo.githubusercontent.com/e3de4ecc6be093ac92a514fa183f688c455b00cc15a3d003bfe2f25e31a08c4f/68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f76322f726573697a653a6669743a313331302f666f726d61743a776562702f302a515a5f4d4a77774c715752314d3136652e706e67" alt="Unit test coverage overview showing test results with percentages for statements, branches, functions, and lines" width="600" height="400" loading="lazy"></a></p>
<p>For files like services and controllers, it's generally better to have a higher unit test coverage, while for files like modules there's no need to write unit tests – nor is it possible to write them, as it's meaningless.</p>
<p>This is because NestJS modules are primarily configuration files that define the structure of your application by connecting controllers, services, and other components together. They don't contain actual business logic to test, but rather serve as wiring instructions for the dependency injection system. Testing modules would only verify that NestJS's core functionality works correctly, which is already tested by the NestJS team themselves.</p>
<p>The above image represents the overall metrics for the entire unit test coverage. If you want to view the test coverage for a specific function, you can open the <code>coverage/lcov-report/index.html</code> file in the project's root directory. For example, I want to see the specific test situation for the <code>validateUser</code> method:</p>
<p><a target="_blank" href="https://camo.githubusercontent.com/e5757001ae5bfec61c2b3ed19f7ef99cffc6c014480d7dad17ab28a2713f6aa0/68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f76322f726573697a653a6669743a313430302f666f726d61743a776562702f302a4e32542d44694d754566776b332d33322e706e67"><img src="https://camo.githubusercontent.com/e5757001ae5bfec61c2b3ed19f7ef99cffc6c014480d7dad17ab28a2713f6aa0/68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f76322f726573697a653a6669743a313430302f666f726d61743a776562702f302a4e32542d44694d754566776b332d33322e706e67" alt="Detailed test coverage for the validateUser method showing specific uncovered lines highlighted in red" width="600" height="400" loading="lazy"></a></p>
<p>As you can see, the original unit test coverage for the <code>validateUser</code> method is not 100%, and there are still two lines of code that were not executed. But it doesn't matter much, as it does not affect the four key processing nodes, and we shouldn’t pursue high test coverage unidimensionally.</p>
<h2 id="heading-how-to-write-e2e-tests">How to Write E2E Tests</h2>
<p>In the unit tests section, you learned how to write unit tests for each feature of the <code>validateUser()</code> function, using mocked data to ensure that each feature could be tested.</p>
<p>In e2E testing, we need to simulate real user scenarios, so connecting to a database for testing is necessary. So, the methods in the <code>auth.service.ts</code> module that we'll be testing all interact with the database.</p>
<p>The <code>auth</code> module primarily includes the following features:</p>
<ul>
<li><p>Registration</p>
</li>
<li><p>Login</p>
</li>
<li><p>Token refresh</p>
</li>
<li><p>Reading user information</p>
</li>
<li><p>Changing password</p>
</li>
<li><p>Deleting users</p>
</li>
</ul>
<p>E2E tests need to test these six features one by one, starting with <code>registration</code> and ending with <code>deleting users</code>. During testing, we can create a dedicated test user to conduct the tests and then delete this test user upon completion, so we don’t leave any unnecessary information in the test database.</p>
<pre><code class="lang-typescript">beforeAll(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> moduleFixture: TestingModule = <span class="hljs-keyword">await</span> Test.createTestingModule({
    imports: [AppModule],
  }).compile()
  app = moduleFixture.createNestApplication()
  <span class="hljs-keyword">await</span> app.init()
  <span class="hljs-comment">// Perform a login to obtain a token</span>
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> request(app.getHttpServer())
    .post(<span class="hljs-string">'/auth/register'</span>)
    .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
    .expect(<span class="hljs-number">201</span>)
  accessToken = response.body.access_token
  refreshToken = response.body.refresh_token
})
afterAll(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> request(app.getHttpServer())
    .delete(<span class="hljs-string">'/auth/delete-user'</span>)
    .set(<span class="hljs-string">'Authorization'</span>, <span class="hljs-string">`Bearer <span class="hljs-subst">${accessToken}</span>`</span>)
    .expect(<span class="hljs-number">200</span>)
  <span class="hljs-keyword">await</span> app.close()
})
</code></pre>
<p>The <code>beforeAll</code> hook function runs before all tests begin, so we can register a test account <code>TEST_USER_NAME</code> here. The <code>afterAll</code> hook function runs after all tests end, so it's suitable to delete the test account <code>TEST_USER_NAME</code> here. It also conveniently tests the registration and deletion functions.</p>
<p>In the previous section's unit tests, we wrote relevant unit tests around the <code>validateUser</code> method. Actually, this method is executed during login to validate if the user's account and password are correct. So this E2E test will also use the login process to demonstrate how to compose the E2E test cases.</p>
<p>The entire login test process includes five small tests:</p>
<pre><code class="lang-typescript">describe(<span class="hljs-string">'login'</span>, <span class="hljs-function">() =&gt;</span> {
    it(<span class="hljs-string">'/auth/login (POST)'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// ...</span>
    })
    it(<span class="hljs-string">'/auth/login (POST) with user not found'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// ...</span>
    })
    it(<span class="hljs-string">'/auth/login (POST) without username or password'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-comment">// ...</span>
    })
    it(<span class="hljs-string">'/auth/login (POST) with invalid password'</span>, <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// ...</span>
    })
    it(<span class="hljs-string">'/auth/login (POST) account lock after multiple failed attempts'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-comment">// ...</span>
    })
  })
</code></pre>
<p>These five tests are as follows:</p>
<ol>
<li><p>Successful login, return 200</p>
</li>
<li><p>If the user does not exist, throw a 401 exception</p>
</li>
<li><p>If password or username is not provided, throw a 400 exception</p>
</li>
<li><p>Login with the wrong password, throw a 401 exception</p>
</li>
<li><p>If the account is locked, throw a 401 exception</p>
</li>
</ol>
<p>Now let's start writing the E2E tests:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// login success</span>
it(<span class="hljs-string">'/auth/login (POST)'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> request(app.getHttpServer())
    .post(<span class="hljs-string">'/auth/login'</span>)
    .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
    .expect(<span class="hljs-number">200</span>)
})
<span class="hljs-comment">// if user not found, should throw 401 exception</span>
it(<span class="hljs-string">'/auth/login (POST) with user not found'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> request(app.getHttpServer())
    .post(<span class="hljs-string">'/auth/login'</span>)
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
    .expect(<span class="hljs-number">401</span>) <span class="hljs-comment">// Expect an unauthorized error</span>
})
</code></pre>
<p>Writing E2E test code is relatively straightforward: you simply call the interface and then verify the result. For example, for the successful login test, we just need to verify that the returned result is 200.</p>
<p>The first four tests are quite simple. Now let's look at a slightly more complicated E2E test, which is to verify whether an account is locked.</p>
<pre><code class="lang-typescript">it(<span class="hljs-string">'/auth/login (POST) account lock after multiple failed attempts'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> moduleFixture: TestingModule = <span class="hljs-keyword">await</span> Test.createTestingModule({
    imports: [AppModule],
  }).compile()
  <span class="hljs-keyword">const</span> app = moduleFixture.createNestApplication()
  <span class="hljs-keyword">await</span> app.init()
  <span class="hljs-keyword">const</span> registerResponse = <span class="hljs-keyword">await</span> request(app.getHttpServer())
    .post(<span class="hljs-string">'/auth/register'</span>)
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
  <span class="hljs-keyword">const</span> accessToken = registerResponse.body.access_token
  <span class="hljs-keyword">const</span> maxLoginAttempts = <span class="hljs-number">3</span> <span class="hljs-comment">// lock user when the third try is failed</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; maxLoginAttempts; i++) {
    <span class="hljs-keyword">await</span> request(app.getHttpServer())
      .post(<span class="hljs-string">'/auth/login'</span>)
      .send({ username: TEST_USER_NAME2, password: <span class="hljs-string">'InvalidPassword'</span> })
  }
  <span class="hljs-comment">// The account is locked after the third failed login attempt</span>
  <span class="hljs-keyword">await</span> request(app.getHttpServer())
    .post(<span class="hljs-string">'/auth/login'</span>)
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
    .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
      expect(res.body.message).toContain(
        <span class="hljs-string">'The account is locked. Please try again in 5 minutes.'</span>,
      )
    })
  <span class="hljs-keyword">await</span> request(app.getHttpServer())
    .delete(<span class="hljs-string">'/auth/delete-user'</span>)
    .set(<span class="hljs-string">'Authorization'</span>, <span class="hljs-string">`Bearer <span class="hljs-subst">${accessToken}</span>`</span>)
  <span class="hljs-keyword">await</span> app.close()
})
</code></pre>
<p>When a user fails to log in three times in a row, the account will be locked. So in this test, we cannot use the test account <code>TEST_USER_NAME</code>, because if the test is successful, this account will be locked and unable to continue the following tests. We need to register another new user <code>TEST_USER_NAME2</code> specifically to test account locking, and delete this user after the test is successful.</p>
<p>So, as you can see, the code for this E2E test is quite substantial, requiring a lot of setup and teardown work, but the actual test code is just these few lines:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// login three times</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; maxLoginAttempts; i++) {
  <span class="hljs-keyword">await</span> request(app.getHttpServer())
    .post(<span class="hljs-string">'/auth/login'</span>)
    .send({ username: TEST_USER_NAME2, password: <span class="hljs-string">'InvalidPassword'</span> })
}
<span class="hljs-comment">// test if the account is locked</span>
<span class="hljs-keyword">await</span> request(app.getHttpServer())
  .post(<span class="hljs-string">'/auth/login'</span>)
  .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
  .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
    expect(res.body.message).toContain(
      <span class="hljs-string">'The account is locked. Please try again in 5 minutes.'</span>,
    )
  })
</code></pre>
<p>Writing E2E test code is relatively simple. You don't need to consider mock data or test coverage. It's sufficient if the entire system process runs as expected.</p>
<h2 id="heading-when-to-write-tests">When to Write Tests</h2>
<p>If possible, I generally recommend writing tests. Doing so can enhance the robustness, maintainability, and development efficiency of the system. Here are some key reasons why writing tests is useful:</p>
<h3 id="heading-enhancing-system-robustness">Enhancing System Robustness</h3>
<p>When writing code, you usually focus on the program flow under normal inputs to ensure the core functionality works properly. But you might often overlook some edge cases, such as abnormal inputs.</p>
<p>Writing tests changes this, as it forces you to consider how to handle these cases and respond appropriately, thus preventing crashes. So we can say that writing tests indirectly improves system robustness.</p>
<h3 id="heading-enhancing-maintainability">Enhancing Maintainability</h3>
<p>Taking over a new project that includes comprehensive tests can be very pleasant. They act as a guide, helping you quickly understand the various functionalities. Just by looking at the test code, you can easily grasp the expected behavior and boundary conditions of each function without having to go through each line of the function's code.</p>
<h3 id="heading-enhancing-development-efficiency">Enhancing Development Efficiency</h3>
<p>Imagine a project that hasn't been updated for a while suddenly receives new requirements. After making changes, you might worry about introducing bugs. Without tests, you would need to manually test the entire project again — wasting time and being inefficient.</p>
<p>With complete tests, a single command can tell you whether the code changes have impacted existing functionalities. Even if there are errors, you can quickly locate them and address them.</p>
<h2 id="heading-when-not-to-write-tests">When Not to Write Tests</h2>
<p>For short-term projects and projects with very fast requirement iterations, it's not recommended to write tests.</p>
<p>For example, a project built for an event that will be useless after the event ends doesn't need tests. Also, for projects that undergo very fast requirement iterations, writing tests could enhance development efficiency, but that's based on the premise that function iterations are slow. If the function you just completed changes in a day or two, the related test code must be rewritten.</p>
<p>So, it's better not to write tests at all in these cases and rely on the testing team instead – because writing tests is very time-consuming and not worth the effort for these situations.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I’ve explained in detail how to write unit tests and E2E tests for NestJS projects. But I still want to reiterate the importance of testing. It can enhance the robustness, maintainability, and development efficiency of the system.</p>
<p>If you don't have the opportunity to write tests, I suggest you start a practice project yourself or participate in some open-source projects and contribute code to them. Open-source projects generally have stricter code requirements. Contributing code may require you to write new test cases or modify existing ones.</p>
<h3 id="heading-reference-materials">Reference Materials</h3>
<ul>
<li><p><a target="_blank" href="https://nestjs.com/">NestJS</a>: A framework for building efficient, scalable Node.js server-side applications.</p>
</li>
<li><p><a target="_blank" href="https://www.mongodb.com/">MongoDB</a>: A NoSQL database used for data storage.</p>
</li>
<li><p><a target="_blank" href="https://jestjs.io/">Jest</a>: A testing framework for JavaScript and TypeScript.</p>
</li>
<li><p><a target="_blank" href="https://github.com/visionmedia/supertest">Supertest</a>: A library for testing HTTP servers.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
