Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Error’d: You Talkin’ to Me?

      September 20, 2025

      The Psychology Of Trust In AI: A Guide To Measuring And Designing For User Confidence

      September 20, 2025

      This week in AI updates: OpenAI Codex updates, Claude integration in Xcode 26, and more (September 19, 2025)

      September 20, 2025

      Report: The major factors driving employee disengagement in 2025

      September 20, 2025

      DistroWatch Weekly, Issue 1140

      September 21, 2025

      Distribution Release: DietPi 9.17

      September 21, 2025

      Development Release: Zorin OS 18 Beta

      September 19, 2025

      Distribution Release: IPFire 2.29 Core 197

      September 19, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      @ts-ignore is almost always the worst option

      September 22, 2025
      Recent

      @ts-ignore is almost always the worst option

      September 22, 2025

      MutativeJS v1.3.0 is out with massive performance gains

      September 22, 2025

      Student Performance Prediction System using Python Machine Learning (ML)

      September 21, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      DistroWatch Weekly, Issue 1140

      September 21, 2025
      Recent

      DistroWatch Weekly, Issue 1140

      September 21, 2025

      Distribution Release: DietPi 9.17

      September 21, 2025

      Hyprland Made Easy: Preconfigured Beautiful Distros

      September 20, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»The Front-End Performance Optimization Handbook – Tips and Strategies for Devs

    The Front-End Performance Optimization Handbook – Tips and Strategies for Devs

    May 8, 2025

    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 keep various performance optimizations in mind – like reducing file size, making fewer server requests, optimizing images in various ways, and so on.

    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.

    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.

    I’ll also provide the references for these optimization suggestions after each one and at the end of the article.

    Table of Contents

    1. Reduce HTTP Requests

    2. Use HTTP2

    3. Use Server-Side Rendering

    4. Use a CDN for Static Resources

    5. Place CSS in the Head and JavaScript Files at the Bottom

    6. Use Font Icons (iconfont) Instead of Image Icons

    7. Make Good Use of Caching, Avoid Reloading the Same Resources

    8. Compress Files

    9. Image Optimization

      • Lazy Loading Images

      • Responsive Images

      • Adjust Image Size

      • Reduce Image Quality

      • Use CSS3 Effects Instead of Images When Possible

      • Use webp Format Images

    10. Load Code on Demand Through Webpack, Extract Third-Party Libraries, Reduce Redundant Code When Converting ES6 to ES5

    11. Reduce Reflows and Repaints

    12. Use Event Delegation

    13. Pay Attention to Program Locality

    14. if-else vs switch

    15. Lookup Tables

    16. Avoid Page Stuttering

    17. Use requestAnimationFrame to Implement Visual Changes

    18. Use Web Workers

    19. Use Bitwise Operations

    20. Don’t Override Native Methods

    21. Reduce the Complexity of CSS Selectors

    22. Use Flexbox Instead of Earlier Layout Models

    23. Use Transform and Opacity Properties to Implement Animations

    24. Use Rules Reasonably, Avoid Over-Optimization

    25. Other References

    26. Conclusion

    1. Reduce HTTP Requests

    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:

    HTTP request waterfall showing timing breakdown

    This is an HTTP request, and the file size is 28.4KB.

    Terminology explained:

    • Queueing: Time spent in the request queue.

    • Stalled: The time difference between when the TCP connection is established and when data can actually be transmitted, including proxy negotiation time.

    • Proxy negotiation: Time spent negotiating with the proxy server.

    • DNS Lookup: Time spent performing DNS lookup. Each different domain on a page requires a DNS lookup.

    • Initial Connection / Connecting: Time spent establishing a connection, including TCP handshake/retry and SSL negotiation.

    • SSL: Time spent completing the SSL handshake.

    • Request sent: Time spent sending the network request, usually a millisecond.

    • Waiting (TFFB): TFFB is the time from when the page request is made until the first byte of response data is received.

    • Content Download: Time spent receiving the response data.

    From this example, we can see that the actual data download time accounts for only 13.05 / 204.16 = 6.39% 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.

    How to combine multiple files

    There are several techniques to reduce the number of HTTP requests by combining files:

    1. Bundle JavaScript files with Webpack

    <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>),
      },
    };
    

    This will combine all JavaScript files imported in your entry point into a single bundle.

    2. Combine CSS files
    Using CSS preprocessors like Sass:

    <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>;
    

    Then compile to a single CSS file:

    sass main.scss:main.css
    

    Reference:

    • Resource_timing

    2. Use HTTP2

    Compared to HTTP1.1, HTTP2 has several advantages:

    Faster parsing

    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.

    Multiplexing

    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.

    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.
    Multiple requests and responses can be sent out of order within the TCP connection and then reassembled at the destination using the stream ID.

    Header compression

    HTTP2 provides header compression functionality.

    For example, consider the following two requests:

    :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>
    
    :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>
    

    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.

    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.

    Here’s a simplified example. Suppose the client sends the following header requests in sequence:

    Header1:foo
    Header2:bar
    Header3:bat
    

    When the client sends a request, it creates a table based on the header values:

    Index Header Name Value
    62 Header1 foo
    63 Header2 bar
    64 Header3 bat

    If the server receives the request, it will create the same table.
    When the client sends the next request, if the headers are the same, it can directly send a header block like this:

    <span class="hljs-number">62</span> <span class="hljs-number">63</span> <span class="hljs-number">64</span>
    

    The server will look up the previously established table and restore these numbers to the complete headers they correspond to.

    Priority

    HTTP2 can set a higher priority for more urgent requests, and the server can prioritize handling them after receiving such requests.

    Flow control

    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.

    Server push

    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.

    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.

    Many websites have already started using HTTP2, such as Zhihu:

    show hot to check HTTP1 and HTTP2 protocols

    Where “h2” refers to the HTTP2 protocol, and “http/1.1” refers to the HTTP1.1 protocol.

    References:

    • HTTP2 Introduction

    • HTTP

    3. Use Server-Side Rendering

    In client-side rendering, you get the HTML file, download JavaScript files as needed, run the files, generate the DOM, and then render.

    And in server-side rendering, the server returns the HTML file, and the client only needs to parse the HTML.

    • Pros: Faster first-screen rendering, better SEO.

    • Cons: Complicated configuration, increases the computational load on the server.

    Below, I’ll use Vue SSR as an example to briefly describe the SSR process.

    Client-side rendering process

    1. Visit a client-rendered website.

    2. The server returns an HTML file containing resource import statements and <div id="app"></div>.

    3. The client requests resources from the server via HTTP, and when the necessary resources are loaded, it executes new Vue() to instantiate and render the page.

    Example of client-side rendered app (Vue):

    <!-- index.html -->
    <!DOCTYPE html>
    <html>
    <head>
      <title>Client-side Rendering Example</title>
    </head>
    <body>
      <!-- Initially empty container -->
      <div id=<span class="hljs-string">"app"</span>></div>
    
      <!-- JavaScript bundle that will render the content -->
      <script src=<span class="hljs-string">"/dist/bundle.js"</span>></script>
    </body>
    </html>
    
    <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> =></span> h(App)
    }).$mount(<span class="hljs-string">'#app'</span>);
    
    <span class="hljs-comment">// App.vue</span>
    <template>
      <div>
        <h1>{{ title }}</h1>
        <p>This content is rendered client-side.</p>
      </div>
    </template>
    
    <script>
    <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>);
      }
    }
    </script>
    

    Server-side rendering process

    1. Visit a server-rendered website.

    2. 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.

    3. 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 new Vue() to instantiate and take over the page.

    Example of server-side rendered app (Vue):

    <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>) =></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>) =></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>);
    
    <!-- index.template.html -->
    <!DOCTYPE html>
    <html>
    <head>
      <title>Server-side Rendering Example</title>
      <!-- Resources injected by the server renderer -->
    </head>
    <body>
      <!-- This will be replaced <span class="hljs-keyword">with</span> the app<span class="hljs-string">'s HTML -->
      <!--vue-ssr-outlet-->
    </body>
    </html></span>
    
    <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 => {
      <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>) =></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">() =></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);
      });
    }
    

    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.

    What’s the benefit of doing this? It’s a faster time-to-content.

    Suppose your website needs to load four files (a, b, c, d) to render completely. And each file is 1 MB in size.

    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). This is why server-side rendering is faster.

    References:

    • vue-ssr-demo

    • Vue.js Server-Side Rendering Guide

    4. Use a CDN for Static Resources

    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.

    CDN Principles

    When a user visits a website without a CDN, the process is as follows:

    1. The browser needs to resolve the domain name into an IP address, so it makes a request to the local DNS.

    2. 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.

    3. 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.

    Diagram showing request flow without CDN: browser → DNS → root servers → top-level domain → authoritative server → website server

    If the user is visiting a website that has deployed a CDN, the process is as follows:

    1. The browser needs to resolve the domain name into an IP address, so it makes a request to the local DNS.

    2. 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.

    3. 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.

    4. The local DNS sends the SLB’s IP address back to the browser, and the browser makes a request to the SLB.

    5. The SLB selects the optimal cache server based on the resource and address requested by the browser and sends it back to the browser.

    6. The browser then redirects to the cache server based on the address returned by the SLB.

    7. 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.

    Diagram showing request flow with CDN: browser → DNS → root servers → GSLB → SLB → cache servers → origin server

    References:

    • Content delivery network(CDN)

    • How to use CDNs to improve performance

    5. Place CSS in the Head and JavaScript Files at the Bottom

    • CSS execution blocks rendering and prevents JS execution

    • JS loading and execution block HTML parsing and prevent CSSOM construction

    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.

    So then you might be wondering – why should CSS files still be placed in the head?

    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.

    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.

    Here’s an example of optimal placement:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset=<span class="hljs-string">"UTF-8"</span>>
      <title>Optimized Resource Loading</title>
    
      <!-- CSS <span class="hljs-keyword">in</span> the head <span class="hljs-keyword">for</span> faster rendering -->
      <link rel=<span class="hljs-string">"stylesheet"</span> href=<span class="hljs-string">"styles.css"</span>>
    
      <!-- Critical JS that must load early can use defer -->
      <script defer src=<span class="hljs-string">"critical.js"</span>></script>
    </head>
    <body>
      <header>
        <h1>My Website</h1>
        <!-- Page content here -->
      </header>
    
      <main>
        <p>Content that users need to see quickly...</p>
      </main>
    
      <footer>
        <!-- Footer content -->
      </footer>
    
      <!-- Non-critical JavaScript at the bottom -->
      <script src=<span class="hljs-string">"app.js"</span>></script>
      <script src=<span class="hljs-string">"analytics.js"</span>></script>
    </body>
    </html>
    

    Explanation of this approach:

    1. CSS in the <head>: 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.

    2. Critical JS with defer: The defer attribute tells the browser to:

      • Download the script in parallel while parsing HTML

      • Only execute the script after HTML parsing is complete but before the DOMContentLoaded event

      • Maintain the order of execution if there are multiple deferred scripts

    3. Non-critical JS before closing </body>: Scripts without special attributes will:

      • Block HTML parsing while they download and execute

      • By placing them at the bottom, we ensure that all the important content is parsed and displayed first

      • This improves perceived performance even if the total load time is the same

    You can also use async for scripts that don’t depend on DOM or other scripts:

    <script <span class="hljs-keyword">async</span> src=<span class="hljs-string">"independent.js"</span>></script>
    

    The async 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.

    Reference:

    • Adding Interactivity with JavaScript

    6. Use Font Icons (iconfont) Instead of Image Icons

    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.

    Compress Font Files

    Use the fontmin-webpack plugin to compress font files (thanks to Frontend Xiaowei for providing this).

    Showing difference between uncompressed and compressed files

    References:

    • fontmin-webpack

    • Iconfont-Alibaba Vector Icon Library

    7. Make Good Use of Caching, Avoid Reloading the Same Resources

    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.

    But this creates a problem: what happens when the file is updated? How do we notify the browser to request the file again?

    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.

    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.

    So what is related to file content? We naturally think of using digest algorithms 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.

    How to implement caching and cache-busting:

    1. Server-side cache headers (using Express.js as an example):

    <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>) =></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>
    });
    

    2. Using content hashes in filenames (Webpack configuration):

    <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>
        })
      ]
    };
    

    This will produce output files like:

    • main.8e0d62a10c151dad4f8e.js

    • styles.f4e3a77c616562b26ca1.css

    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.

    3. Example of generated HTML with cache-busting:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset=<span class="hljs-string">"UTF-8"</span>>
      <title>Cache Busting Example</title>
      <!-- Note the content hash <span class="hljs-keyword">in</span> the filename -->
      <link rel=<span class="hljs-string">"stylesheet"</span> href=<span class="hljs-string">"/static/styles.f4e3a77c616562b26ca1.css"</span>>
    </head>
    <body>
      <div id=<span class="hljs-string">"app"</span>></div>
      <!-- Script <span class="hljs-keyword">with</span> content hash -->
      <script src=<span class="hljs-string">"/static/main.8e0d62a10c151dad4f8e.js"</span>></script>
    </body>
    </html>
    

    4. Version query parameters (simpler but less effective approach):

    <link rel=<span class="hljs-string">"stylesheet"</span> href=<span class="hljs-string">"styles.css?v=1.2.3"</span>>
    <script src=<span class="hljs-string">"app.js?v=1.2.3"</span>></script>
    

    When updating files, manually change the version number to force a new download.

    References:

    • webpack-caching

    8. Compress Files

    Compressing files can reduce file download time, providing a better user experience.

    Thanks to the development of Webpack and Node, file compression is now very convenient.

    In Webpack, you can use the following plugins for compression:

    • JavaScript: UglifyPlugin

    • CSS: MiniCssExtractPlugin

    • HTML: HtmlWebpackPlugin

    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.

    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%.

    Here are the methods for configuring gzip in webpack and node.

    Download plugins

    npm install compression-webpack-plugin --save-dev
    npm install compression
    

    Webpack configuration

    <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()],
    }
    

    Node configuration

    <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())
    

    9. Image Optimization

    1. Lazy Loading Images

    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.

    First, set up the images like this, where images won’t load when they’re not visible in the page:

    <img data-src=<span class="hljs-string">"https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4"</span>>
    

    When the page becomes visible, use JS to load the image:

    <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
    

    This is how the image gets loaded. For the complete code, please refer to the reference materials.

    Reference:

    • Lazy loading images for the web

    2. Responsive Images

    The advantage of responsive images is that browsers can automatically load appropriate images based on screen size.

    Implementation through picture:

    <picture>
        <source srcset=<span class="hljs-string">"banner_w1000.jpg"</span> media=<span class="hljs-string">"(min-width: 801px)"</span>>
        <source srcset=<span class="hljs-string">"banner_w800.jpg"</span> media=<span class="hljs-string">"(max-width: 800px)"</span>>
        <img src=<span class="hljs-string">"banner_w800.jpg"</span> alt=<span class="hljs-string">""</span>>
    </picture>
    

    Implementation through @media:

    <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);
        }
    }
    

    3. Adjust Image Size

    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.

    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.

    Example implementation of image size optimization:

    <!-- HTML Structure -->
    <div <span class="hljs-keyword">class</span>=<span class="hljs-string">"image-container"</span>>
      <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>>
      <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>>
    </div>
    
    <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;
    }
    
    <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">() =></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> =></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">() =></span> {
          <span class="hljs-keyword">if</span> (!fullSize.src && 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', () => {
          setTimeout(() => {
            if (!fullSize.src && fullSize.dataset.src) {
              fullSize.src = fullSize.dataset.src;
            }
          }, 1000); // Delay loading by 1 second after window load
        });
        */</span>
      });
    });
    

    This implementation:

    1. Shows only the thumbnail initially

    2. Loads the full-size image only when the user hovers over the thumbnail

    3. Provides an alternative approach to load all full-size images with a delay after page load

    4. Reduce Image Quality

    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.

    There are two compression methods: one is through the Webpack plugin image-webpack-loader, and the other is through online compression websites.

    Here’s how to use the Webpack plugin image-webpack-loader:

    npm i -D image-webpack-loader
    

    Webpack configuration:

    {
      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>,
          }
        }
      ]
    }
    

    5. Use CSS3 Effects Instead of Images When Possible

    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.

    Reference:

    • Asset Management

    6. Use WebP to Format Images

    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.

    Example of implementing WebP with fallbacks:

    <!-- Using the picture element <span class="hljs-keyword">for</span> WebP <span class="hljs-keyword">with</span> fallback -->
    <picture>
      <source srcset=<span class="hljs-string">"image.webp"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"image/webp"</span>>
      <source srcset=<span class="hljs-string">"image.jpg"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"image/jpeg"</span>>
      <img src=<span class="hljs-string">"image.jpg"</span> alt=<span class="hljs-string">"Description of the image"</span>>
    </picture>
    

    Server-side WebP detection and serving:

    <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>) =></span> {
      <span class="hljs-keyword">const</span> supportsWebP = req.headers.accept && 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));
    });
    

    Reference:

    • WebP

    10. Load Code on Demand Through Webpack, Extract Third-Party Libraries, Reduce Redundant Code When Converting ES6 to ES5

    The following quote from the official Webpack documentation explains the concept of lazy loading:

    “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.” Source: Lazy Loading

    Note: 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.

    Generate File Names Based on File Content, Combined with Import Dynamic Import of Components to Achieve On-Demand Loading

    This requirement can be achieved by configuring the filename property of output. One of the value options in the filename property is [contenthash], which creates a unique hash based on file content. When the file content changes, [contenthash] also changes.

    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>),
    },
    

    Example of code lazy loading in a Vue application:

    <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">() =></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 }
      ]
    })
    

    This ensures the UserProfile component is only loaded when a user navigates to that route, not on initial page load.

    Extract Third-Party Libraries

    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.

    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>
                }
            },
        }
    },
    
    • test: 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: RegExp, String, and Function.

    • priority: Indicates extraction weight, with higher numbers indicating higher priority. Since a module might meet the conditions of multiple cacheGroups, extraction is determined by the highest weight.

    • reuseExistingChunk: 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.

    • minChunks (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).

    • chunks (default is async): initial, async, and all.

    • name (name of the packaged chunks): String or function (functions can customize names based on conditions).

    Reduce Redundant Code When Converting ES6 to ES5

    To achieve the same functionality as the original code after Babel conversion, some helper functions are needed. For example this:

    <span class="hljs-keyword">class</span> Person {}
    

    will be converted to this:

    <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);
    };
    

    Here, _classCallCheck is a helper function. If classes are declared in many files, then many such helper functions will be generated.

    The @babel/runtime package declares all the helper functions needed, and the role of @babel/plugin-transform-runtime is to import all files that need helper functions from the @babel/runtime package:

    <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 && 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);
    };
    

    Here, the helper function classCallCheck is no longer compiled, but instead references helpers/classCallCheck from @babel/runtime.

    Installation:

    npm i -D <span class="hljs-meta">@babel</span>/plugin-transform-runtime <span class="hljs-meta">@babel</span>/runtime
    

    Usage:
    In the .babelrc file,

    <span class="hljs-string">"plugins"</span>: [
            <span class="hljs-string">"@babel/plugin-transform-runtime"</span>
    ]
    

    References:

    • Babel

    • Vue Route Lazy Loading

    • SplitChunksPlugin

    11. Reduce Reflows and Repaints

    Browser Rendering Process

    1. Parse HTML to generate DOM tree.

    2. Parse CSS to generate CSSOM rules tree.

    3. Combine DOM tree and CSSOM rules tree to generate rendering tree.

    4. Traverse the rendering tree to begin layout, calculating the position and size information of each node.

    5. Paint each node of the rendering tree to the screen.

    Diagram of browser rendering process showing the steps from HTML/CSS to rendered pixels

    Reflow

    When the position or size of DOM elements is changed, the browser needs to regenerate the rendering tree, a process called reflow.

    Repaint

    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.

    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.

    What operations will cause reflow?

    • Adding or removing visible DOM elements

    • Element position changes

    • Element size changes

    • Content changes

    • Browser window size changes

    How to reduce reflows and repaints?

    • When modifying styles with JavaScript, it’s best not to write styles directly, but to replace classes to change styles.

    • 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.

    Example of causing unnecessary reflows (inefficient):

    <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>;
    

    Optimized version 1 – using CSS classes:

    <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;
    }
    
    <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>);
    

    Optimized version 2 – batching style changes:

    <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>;
    

    Optimized version 3 – using document fragments (for multiple elements):

    <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 < <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);
    

    Optimized version 4 – take element out of flow, modify, then reinsert:

    <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);
    }
    

    Optimized version 5 – using display:none temporarily:

    <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>;
    

    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.

    12. Use Event Delegation

    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.

    <ul>
      <li>Apple</li>
      <li>Banana</li>
      <li>Pineapple</li>
    </ul>
    
    <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>) =></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>) =></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)
      }
    })
    

    13. Pay Attention to Program Locality

    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.

    Locality usually takes two different forms:

    • 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.

    • 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.

    Temporal locality example:

    <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 < len; i++) {
            sum += arry[i]
        }
    
        <span class="hljs-keyword">return</span> sum
    }
    

    In this example, the variable sum is referenced once in each loop iteration, so it has good temporal locality.

    Spatial locality example:

    Program with good spatial locality:

    <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 < rows; i++) {
            <span class="hljs-keyword">for</span> (j = <span class="hljs-number">0</span>; j < cols; j++) {
                sum += arry[i][j]
            }
        }
        <span class="hljs-keyword">return</span> sum
    }
    

    Program with poor spatial locality:

    <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 < cols; j++) {
            <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i < rows; i++) {
                sum += arry[i][j]
            }
        }
        <span class="hljs-keyword">return</span> sum
    }
    

    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.

    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.

    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.

    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.

    Performance Testing

    Running environment:

    • CPU: i5-7400

    • Browser: Chrome 70.0.3538.110

    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:

    The examples used are the two spatial locality examples mentioned above.

    Stride 1 Stride 9000
    124 2316

    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.

    So to sum up:

    • Programs that repeatedly reference the same variables have good temporal locality

    • 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

    Reference:

    • Computer Systems: A Programmer’s Perspective

    14. if-else vs switch

    As the number of judgment conditions increases, it becomes more preferable to use switch instead of if-else.

    <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>
    }
    

    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).

    Why switch is better for multiple conditions:

    1. Improved readability: 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.

    2. Cleaner code maintenance: 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.

    3. Less repetition: In the if-else example, we repeat checking the same variable (color) multiple times, while in switch we specify it once at the top.

    4. Better for debugging: 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.

    5. Intent signaling: Using switch communicates to other developers that you’re checking multiple possible values of the same variable, rather than potentially unrelated conditions.

    For modern JavaScript, there’s another alternative worth considering for simple value mapping – object literals:

    <span class="hljs-keyword">const</span> colorActions = {
      <span class="hljs-string">'blue'</span>: <span class="hljs-function">() =></span> { <span class="hljs-comment">/* blue action */</span> },
      <span class="hljs-string">'yellow'</span>: <span class="hljs-function">() =></span> { <span class="hljs-comment">/* yellow action */</span> },
      <span class="hljs-string">'white'</span>: <span class="hljs-function">() =></span> { <span class="hljs-comment">/* white action */</span> },
      <span class="hljs-string">'black'</span>: <span class="hljs-function">() =></span> { <span class="hljs-comment">/* black action */</span> },
      <span class="hljs-string">'green'</span>: <span class="hljs-function">() =></span> { <span class="hljs-comment">/* green action */</span> },
      <span class="hljs-string">'orange'</span>: <span class="hljs-function">() =></span> { <span class="hljs-comment">/* orange action */</span> },
      <span class="hljs-string">'pink'</span>: <span class="hljs-function">() =></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]();
    }
    

    This approach provides even better performance (O(1) lookup time) compared to both if-else and switch statement approaches.

    15. Lookup Tables

    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.

    <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
    }
    

    This switch statement can be converted to a lookup table:

    <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]
    

    If the conditional statements are not numerical values but strings, you can use an object to build a lookup table:

    <span class="hljs-keyword">const</span> map = {
      red: result0,
      green: result1,
    }
    
    <span class="hljs-keyword">return</span> map[color]
    

    Why lookup tables are better for many conditions:

    1. Constant time complexity (O(1)): 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.

    2. Performance gains with many conditions: 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.

    3. Code brevity: As shown in the examples, lookup tables typically require less code, making your codebase more maintainable.

    4. Dynamic configuration: Lookup tables can be easily populated dynamically:

       <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]();
       }
    
    1. Reduced cognitive load: When there are many conditions, lookup tables eliminate the mental overhead of following long chains of logic.

    When to use each approach:

    • If-else: Best for a few conditions (2-3) with complex logic or different variables being checked

    • Switch: Good for moderate number of conditions (4-10) checking against the same variable

    • Lookup tables: Ideal for many conditions (10+) or when you need O(1) access time

    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.

    16. Avoid Page Stuttering

    60fps and Device Refresh Rate

    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.

    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.

    This phenomenon is commonly known as stuttering and has a negative impact on user experience. Source: Google Web Fundamentals – Rendering Performance

    Frame budget timing diagram showing the 16ms frame budget and browser overhead

    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.

    For some long-running JavaScript, we can use timers to split and delay execution.

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>, len = arry.length; i < len; i++) {
        process(arry[i])
    }
    

    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.

    <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>)
    

    If you’re interested in learning more, check out High Performance JavaScript Chapter 6.

    Reference:

    • Rendering Performance

    17. Use requestAnimationFrame to Implement Visual Changes

    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 requestAnimationFrame.

    <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);
    

    If you use setTimeout or setInterval 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.

    show the execution time of javascript

    Reference:

    • Optimize JavaScript Execution

    • Improve JS performance

    18. Use Web Workers

    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).

    Web Workers are suitable for processing pure data or long-running scripts unrelated to the browser UI.

    Creating a new worker is simple – just specify a script URI to execute the worker thread (main.js):

    <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>);
    }
    

    In the worker, after receiving the message, you can write an event handler function code as a response (worker.js):

    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);
    }
    

    The onmessage 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 postMessage() method again to send the result back to the main thread.

    Back in the main thread, we use onmessage again to respond to the message sent back from the worker:

    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>);
    }
    

    Here we get the data from the message event and set it as the textContent of result, so the user can directly see the result of the calculation.

    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.

    Reference:

    • Web Workers

    • How web workers work in JS

    19. Use Bitwise Operations

    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.

    Modulo

    Since the lowest bit of even numbers is 0 and odd numbers is 1, modulo operations can be replaced with bitwise operations.

    <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 & <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>
    }
    

    How it works: The & (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.

    When we do value & 1, we’re only checking the last bit of the number:

    • For even numbers (for example, 4 = 100 in binary), the last bit is 0: 100 & 001 = 000 (0)

    • For odd numbers (for example, 5 = 101 in binary), the last bit is 1: 101 & 001 = 001 (1)

    Floor

    ~~<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>
    

    How it works: The ~ (bitwise NOT) operator inverts all bits in the operand. For a number n, ~n equals -(n+1). When applied twice (~~n), it effectively truncates the decimal part of a number, similar to Math.floor() for positive numbers and Math.ceil() for negative numbers.

    The process:

    1. First ~: Converts the number to a 32-bit integer and inverts all bits

    2. Second ~: Inverts all bits again, resulting in the original number but with decimal part removed

    For example:

    ~<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>
    

    Bitmask

    <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
    

    By defining these options, you can use the bitwise AND operation to determine if a/b/c is in the options.

    <span class="hljs-comment">// Is option b in the options?</span>
    <span class="hljs-keyword">if</span> (b & options) {
        ...
    }
    

    How it works: In bitmasks, each bit represents a boolean flag. The values are typically powers of 2 so each has exactly one bit set.

    1. a = 1: Binary 001

    2. b = 2: Binary 010

    3. c = 4: Binary 100

    4. options = a | b | c: The | (bitwise OR) combines them: 001 | 010 | 100 = 111 (binary) = 7 (decimal)

    When checking if a flag is set with if (b & options):

    • b & options = 010 & 111 = 010 = 2 (decimal)

    • Since this is non-zero, the condition evaluates to true

    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.

    20. Don’t Override Native Methods

    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.

    Example: String Replacement (Native vs. Custom)

    A common pitfall is rewriting native string methods like replaceAll(). Below is an inefficient custom implementation versus the native method, with performance benchmarks:

    <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 < 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>
    

    Key takeaways:

    • Performance: Native methods like replaceAll() are optimized at the browser level, often outperforming handwritten code (as shown in the benchmark above).

    • Maintainability: Native methods are standardized, well-documented, and less error-prone than custom logic (for example, handling edge cases like overlapping substrings).

    • Ecosystem compatibility: Using native methods ensures consistency with libraries and tools that rely on JavaScript’s built-in behavior.

    When to Use Custom Code

    While native methods are usually superior, there are rare cases where you might need custom logic:

    • When the native method doesn’t exist (for example, polyfilling for older browsers).

    • For highly specialized edge cases not covered by native APIs.

    • When you need to avoid function call overhead in extremely performance-critical loops (for example, tight numerical computations).

    Remember: 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.

    21. Reduce the Complexity of CSS Selectors

    1. When browsers read selectors, they follow the principle of reading from right to left.

    Let’s look at an example:

    #block .text p {
        color: red;
    }
    
    1. Find all P elements.

    2. Check if the elements found in result 1 have parent elements with class name “text”

    3. Check if the elements found in result 2 have parent elements with ID “block”

    Why is this inefficient? This right-to-left evaluation process can be very expensive in complex documents. Take the selector #block .text p as an example:

    1. The browser first finds all p elements in the document (potentially hundreds)

    2. For each of those paragraph elements, it must check if any of their ancestors have the class text

    3. For those that pass step 2, it must check if any of their ancestors have the ID block

    This creates a significant performance bottleneck because:

    • The initial selection (p) is very broad

    • Each subsequent step requires checking multiple ancestors in the DOM tree

    • This process repeats for every paragraph element

    A more efficient alternative would be:

    #block p.specific-text {
        color: red;
    }
    

    This is more efficient because it directly targets only paragraphs with a specific class, avoiding checking all paragraphs

    2. CSS selector priority

    Inline > ID selector > Class selector > Tag selector
    

    Based on the above two pieces of information, we can draw conclusions:

    1. The shorter the selector, the better.

    2. Try to use high-priority selectors, such as ID and class selectors.

    3. Avoid using the universal selector *.

    Practical advice for optimal CSS selectors:

    <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;
    }
    

    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.

    Reference:

    • Optimizing CSS: ID Selectors and Other Myths

    22. Use Flexbox Instead of Earlier Layout Models

    In early CSS layout methods, we could position elements absolutely, relatively, or using floats. Now, we have a new layout method called Flexbox, which has an advantage over earlier layout methods: better performance.

    The screenshot below shows the layout cost of using floats on 1300 boxes:

    layout timeline in dev tool

    Then we recreate this example using Flexbox:

    layout timeline in dev tool

    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).

    But Flexbox compatibility is still an issue, as not all browsers support it, so use it with caution.

    Browser compatibility:

    • Chrome 29+

    • Firefox 28+

    • Internet Explorer 11

    • Opera 17+

    • Safari 6.1+ (prefixed with -webkit-)

    • Android 4.4+

    • iOS 7.1+ (prefixed with -webkit-)

    Reference:

    • Use flexbox instead of earlier layout models

    23. Use Transform and Opacity Properties to Implement Animations

    In CSS, transforms and opacity property changes don’t trigger reflow and repaint. They’re properties that can be processed by the compositor alone.

    Diagram showing how transform and opacity properties bypass layout and paint processes

    Example: Inefficient vs. Efficient Animation

    ❌ Inefficient animation using properties that trigger reflow and repaint:

    <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;
      }
    }
    

    This animation constantly triggers layout recalculations (reflow) because it animates position (left/top) and size (width/height) properties.

    ✅ Efficient animation using transform and opacity:

    <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>;
      }
    }
    

    Why this is better:

    1. transform: translate(300px, 200px) replaces left: 300px; top: 200px

    2. transform: scale(1.5) replaces width: 150px; height: 150px

    3. These transform operations and opacity changes can be handled directly by the GPU without triggering layout or paint operations

    Performance comparison:

    1. The inefficient version may drop frames on lower-end devices because each frame requires:

      • JavaScript → Style calculations → Layout → Paint → Composite
    2. The efficient version typically maintains 60fps because it only requires:

      • JavaScript → Style calculations → Composite

    HTML implementation:

    <div <span class="hljs-keyword">class</span>=<span class="hljs-string">"box-inefficient"</span>>Inefficient</div>
    <div <span class="hljs-keyword">class</span>=<span class="hljs-string">"box-efficient"</span>>Efficient</div>
    

    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.

    Reference:

    • Use transform and opacity property changes to implement animations

    24. Use Rules Reasonably, Avoid Over-Optimization

    Performance optimization is mainly divided into two categories:

    1. Load-time optimization

    2. Runtime optimization

    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.

    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.

    Check Loading Performance

    A website’s loading performance mainly depends on white screen time and first screen time.

    • White screen time: The time from entering the URL to when the page starts displaying content.

    • First screen time: The time from entering the URL to when the page is completely rendered.

    You can get the white screen time by placing the following script before </head>.

    <script>
      <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
    </script>
    

    You can get the first screen time by executing new Date() - performance.timing.navigationStart in the window.onload event.

    Check Runtime Performance

    With Chrome’s developer tools, we can check the website’s performance during runtime.

    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.

    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.

    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!

    References:

    • performance.timing.navigationStart

    Conclusion

    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.

    Key Takeaways

    1. Start with measurement, not optimization. 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.

    2. Focus on the critical rendering path. 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.

    3. Understand the browser rendering process. 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.

    4. Balance implementation cost vs. performance gain. 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.

    5. Consider the device and network conditions of your users. 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.

    Practical Implementation Strategy

    Instead of trying to implement all 24 techniques at once, consider taking a phased approach:

    1. First pass: Implement the easy wins with high impact

      • Proper image optimization

      • HTTP/2

      • Basic caching

      • CSS/JS placement

    2. Second pass: Address specific measured bottlenecks

      • Use performance profiling to identify problem areas

      • Apply targeted optimizations based on findings

    3. Ongoing maintenance: Make performance part of your development workflow

      • Set performance budgets

      • Implement automated performance testing

      • Review new feature additions for performance impact

    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.

    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.

    Other References

    • Why Performance Matters

    • High-Performance Website Construction Guide

    • High Performance Browser Networking

    • High-Performance JavaScript

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleRecursive Types in TypeScript: A Brief Exploration
    Next Article 3 of NVIDIA’s biggest red flags I can’t ignore for its upcoming RTX 5060 launch

    Related Posts

    Development

    @ts-ignore is almost always the worst option

    September 22, 2025
    Development

    MutativeJS v1.3.0 is out with massive performance gains

    September 22, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    GPT Proto – Unified Access to All AI Models via Fast & Secure APIs

    Web Development

    What’s new in ECMAScript 2025

    Development

    Intel’s August 2025 driver for Windows 11 24H2 boosts performance, adds GPU memory override control

    Operating Systems

    This proxy provider I tested is the best for web scraping – and it’s not IPRoyal or MarsProxies

    News & Updates

    Highlights

    Development

    How Web Services Work – The Unseen Engines of the Connected World

    May 14, 2025

    Have you ever wondered how your weather app instantly knows the forecast, how you can…

    Integrating Localization Into Design Systems

    May 12, 2025

    CVE-2025-6302 – TOTOLINK EX1200T Stack-Based Buffer Overflow Vulnerability

    June 20, 2025

    CVE-2025-6855 – “Chatchat-Langchain Chatchat Path Traversal Vulnerability”

    June 29, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.