前端效能最佳化實踐方向與方法

SRIGT發表於2024-07-26

0x01 程式碼最佳化與壓縮

(1)HTML

移除不必要的空白字元、註釋和冗餘標籤,以減少檔案大小

  1. 使用命令 npm install html-minifier -g 安裝 HTML Minifier

  2. 使用命令 html-minifier -V 確認安裝成功

  3. 在 Node.js 環境中配置 index.js

    // 引入 HTML Minifier
    const minify = require("html-minifier").minify;
    
    // 處理 HTML 文字
    let result = minify('<p title="blah" id="moo">foo</p>', {
      removeAttributeQuotes: true,
    });
    
    // 輸出處理結果
    console.log(result);
    
  4. 使用命令 node .\index.js 執行,輸出結果為:<p title=blah id=moo>foo</p>

  5. 詳細參考:https://www.npmjs.com/package/html-minifier
    線上使用:https://kangax.github.io/html-minifier/

(2)CSS

精簡樣式表,避免使用冗餘或過時的屬性,合理組織選擇器以減少計算複雜度

  1. 使用命令 npm install cssnano postcss postcss-cli --save-dev 安裝 PostCSS 與 CSSNaNo

  2. 在 Node.js 環境中配置 postcss.config.js

    module.exports = {
      plugins: [
        require("cssnano")({
          preset: "default",
        }),
      ],
    };
    
  3. 使用命令 npx postcss input.css > output.css 執行,生成最佳化後的結果 output.css

  4. 詳細參考:https://www.cssnano.cn/docs/introduction/

  5. 其他工具:

    1. PostCSS:https://www.postcss.com.cn/
    2. PurgeCSS:https://www.purgecss.cn/

(3)JavaScript

使用工具(如 Webpack 等)或線上服務對程式碼進行壓縮,移除空格、註釋和不必要的字元

  1. 使用命令 npm install terser-webpack-plugin --save-dev 安裝 terser-webpack-plugin

  2. 在 Node.js 環境中配置 webpack.config.js

    const TerserPlugin = require("terser-webpack-plugin");
    
    module.exports = {
      optimization: {
        minimize: true,
        minimizer: [new TerserPlugin()],
      },
    };
    
  3. 使用命令 npx webpack 執行 Webpack 打包工具,並最佳化 JavaScript 程式碼

(4)合併檔案

將多個 CSS、JavaScript 檔案合併為一個,減少 HTTP 請求的數量

  • 在 Webpack 中,可以透過配置 entryoutput 選項來自動合併多個模組到一個檔案中
  • 在 Gulp 中,可以使用 gulp-concat 外掛來合併檔案

0x02 靜態資源最佳化

(1)壓縮圖片

  • TinyPNG:API Reference
  • ImageOptim:Optimize on the fly
  • imagemin:https://www.npmjs.com/package/imagemin

(2)圖片懶載入

a. 原生 JavaScript

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <style>
    img {
      width: 1000px;
      height: 700px;
      background-color: wheat;
      object-fit: cover;
      object-position: center;
    }
  </style>
</head>

<body>
  <img src="" data-src="./images/1.jpg" alt="" />
  <img src="" data-src="./images/2.jpg" alt="" />
  <img src="" data-src="./images/3.jpg" alt="" />
  <img src="" data-src="./images/4.jpg" alt="" />
  <img src="" data-src="./images/5.jpg" alt="" />
  <script>
    /**
     * 初始化圖片懶載入功能。
     * 該函式透過監聽視窗的滾動事件,來實現圖片的延遲載入。當圖片進入視口時,將其src屬性設定為真正的圖片源URL,從而實現懶載入的效果。
     */
    const imageLazyLoad = () => {
      // 獲取頁面中所有帶有data-src屬性的圖片元素
      const imgs = document.querySelectorAll("img");
      // 定義計算函式,用於檢查圖片是否進入視口
      const calc = () => {
        imgs.forEach((img) => {
          // 檢查圖片是否進入視口:如果圖片的頂部位置小於等於視窗的底部位置,則圖片已進入視口,可以載入
          if (img.offsetTop <= window.innerHeight + window.scrollY)
            // 設定圖片的src屬性為data-src屬性的值,真正開始載入圖片
            img.src = img.dataset.src;
          else
            // 如果圖片未進入視口,則返回,不進行載入
            return;
        });
      };
      // 監聽視窗的滾動事件,以便在滾動時觸發圖片的載入
      window.addEventListener("scroll", () => calc());
      // 初次載入頁面時,立即計算並載入可視區域內的圖片
      calc();
    };
    // 呼叫函式,初始化圖片懶載入
    imageLazyLoad();
  </script>
</body>

</html>

b. Intersection Observer API

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <style>
    img {
      width: 1000px;
      height: 700px;
      background-color: wheat;
      object-fit: cover;
      object-position: center;
    }
  </style>
</head>

<body>
  <img src="" data-src="./images/1.jpg" alt="" />
  <img src="" data-src="./images/2.jpg" alt="" />
  <img src="" data-src="./images/3.jpg" alt="" />
  <img src="" data-src="./images/4.jpg" alt="" />
  <img src="" data-src="./images/5.jpg" alt="" />
  <script>
    /**
     * 初始化圖片懶載入
     * 該函式透過IntersectionObserver API來實現圖片的懶載入。只有當圖片進入視口時,才會真正載入圖片資源
     */
    const imageLazyLoad = () => {
      // 獲取所有需要懶載入的圖片元素
      const imgs = document.querySelectorAll("img");

      // 建立IntersectionObserver例項,用於觀察圖片是否進入視口
      const observer = new IntersectionObserver((entries) => {
        // 遍歷所有觀察到的條目
        entries.forEach((entry) => {
          // 如果圖片進入視口
          if (entry.isIntersecting) {
            let img = entry.target;
            // 設定圖片的src屬性為data-src屬性的值,即真正的圖片源地址
            img.src = img.dataset.src;
            // 停止觀察該圖片,因為它已經載入
            observer.unobserve(img);
          }
        });
      });

      // 對所有需要懶載入的圖片元素啟用IntersectionObserver觀察
      imgs.forEach((img) => {
        observer.observe(img);
      });
    };
    imageLazyLoad();
  </script>
</body>

</html>

c. 第三方庫

  • vue-lazyload(Vue 框架中使用)

(3)CDN

  • 部署內容分發網路(CDN),將靜態資源託管在地理位置接近使用者的邊緣節點上,減少延遲
  • 步驟:
    1. 選擇 CDN 服務提供商,如 Amazon、Cloudflare 等
    2. 上傳靜態資源
    3. 配置 DNS
    4. 測試並最佳化
  • 優點:減少訪問延遲、提高可用性、減輕源伺服器負載

(4)快取機制

  • 步驟:
    1. 設定 Cache-Control 頭部:響應頭欄位,表示在快取有效期內直接從本地快取中載入這些資源,而不是向伺服器傳送請求
    2. 使用 ETag:響應頭欄位,表示資源的特定版本
      • 當瀏覽器再次請求資源時,會將 ETag 值傳送給伺服器,如果資源未更改(即 ETag 值相同),伺服器將返回 304 Not Modified 響應,告訴瀏覽器使用本地快取的版本。
    3. 配置 Expires 頭部:相容舊版瀏覽器
  • 優點:減少伺服器負載、加快載入速度、改善使用者體驗

0x03 渲染效能最佳化

(1)減少 DOM 元素數量

避免不必要的 DOM 元素,以減少渲染和重繪的時間

  1. 使用 CSS 替代 DOM 元素

    舉例:

    <ul>
      <li><img src="icon1.png" alt="Icon 1"><span>Item 1</span></li>
      <li><img src="icon2.png" alt="Icon 2"><span>Item 2</span></li>
    </ul>
    

    最佳化為

    <ul>
      <li class="item">Item 1</li>
      <li class="item">Item 2</li>
    </ul>
    
    <style>
    .item::before {
      content: "";
      display: inline-block;
      width: 20px;
      height: 20px;
      background-image: url(icon-based-on-class.png);
      background-size: cover;
      margin-right: 5px;
    }
    </style>
    
  2. 引入 Flex 佈局與 Grid 佈局

    舉例:

    <div class="container">
      <div class="row">
        <div class="col">Item 1</div>
        <div class="col">Item 2</div>
      </div>
    </div>
    

    最佳化為

    <div style="display: flex;">
      <div>Item 1</div>
      <div>Item 2</div>
    </div>
    
  3. 多個動態內容採用 DocumentFragment 新增

    舉例:

    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      li.textContent = `Item ${i}`;
      document.querySelector("ul").appendChild(li);
    }
    

    最佳化為

    let fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      li.textContent = `Item ${i}`;
      fragment.appendChild(li);
    }
    document.querySelector("ul").appendChild(fragment);
    

    DocumentFragment 是一個輕量級的文件物件,可以包含節點和子節點,但不會成為文件樹的一部分

(2)事件委託

透過事件委託來減少與 DOM 的互動次數,提高效能

  • 事件委託:一種事件處理的技術,它利用事件冒泡的原理,只在父元素上設定一個事件監聽器,而不是在每個子元素上分別設定。當子元素上發生事件時,該事件會冒泡到父元素,父元素上的事件監聽器會檢查事件源(即觸發事件的子元素),並據此執行相應的操作

  • 舉例

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8" />
    </head>
    
    <body>
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
      <button>Add item</button>
      <script>
        const ul = document.querySelector("ul");
    
        ul.addEventListener("click", (e) => {
          if (e.target.tagName === "LI") {
            console.log("e.target.textContent", e.target.textContent);
            e.target.remove();
          }
        });
    
        const button = document.querySelector("button");
        button.addEventListener("click", () => {
          const newItem = document.createElement("li");
          newItem.textContent = `Item ${ul.children.length + 1}`;
          ul.appendChild(newItem);
        });
      </script>
    </body>
    
    </html>
    

(3)最佳化 DOM 操作

使用批次更新、虛擬 DOM 等技術減少重繪和迴流

  1. 批次更新:將多個 DOM 操作合併成一個批次,然後一次性執行(參考 DocumentFragment)
  2. 虛擬 DOM:一種程式設計概念,用 JavaScript 物件來表示 DOM 樹
    • 當應用程式的狀態發生變化時,虛擬 DOM 樹會首先進行更新,然後使用高效的演算法(如 diff 演算法)來比較新舊虛擬 DOM 樹之間的差異,並只將這些差異應用到真實的 DOM 上
    • 即檢測發生變化的 DOM 元素,並僅對變化的 DOM 元素操作,減少不必要的 DOM 操作
    • 一般在 React、Vue 等前端框架中廣泛應用

(4)CSS 放在頂部

  • 將CSS放在 <head> 標籤內,以確保頁面在載入時能夠優先渲染樣式

    <head>
      <link rel="stylesheet" href="style.css" />
    </head>
    

(5)非同步載入 JavaScript

  • 將非首屏必需的 JS 指令碼放在文件末尾或使用 asyncdefer 屬性,避免阻塞渲染

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <!-- 同步載入首屏必需指令碼 -->
      <script src="critical.js"></script>
    </head>
    
    <body>
      <!-- 頁面內容 -->
    
      <!-- 非同步載入非首屏且無依賴的指令碼 -->
      <script async src="non-critical-async.js"></script>
    
      <!-- 非同步載入但按順序執行的非首屏指令碼 -->
      <script defer src="non-critical-defer.js"></script>
    
      <!-- 將非首屏指令碼放在底部(適用於不支援 async/defer 的老舊瀏覽器) -->
      <!-- <script src="non-critical-old-browser.js"></script> -->
    </body>
    
    </html>
    
  • asyncdefer

    • 兩者都用於非同步載入指令碼
    • async:完全非同步,指令碼的載入和執行不會阻塞文件的解析,但多個非同步指令碼之間執行順序不一定,不建議用於具有依賴關係的指令碼
    • defer:等到整個文件都被解析和顯示之後,再按照指令碼在文件中出現的順序來執行

0x04 網路效能最佳化

(1)啟用 HTTP/2 或 HTTP/3

  • 啟用 HTTP/2 或 HTTP/3,利用多路複用、頭部壓縮等特性提升請求效率
  • 一般在 Web 伺服器中,透過 Nginx、Apache 等配置並啟用

(2)TLS/SSL

  • 確保網站使用 HTTPS 加密傳輸,提高安全性並可能獲得 SEO 優勢
  • HTTPS、TLS/SSL 在網路傳輸的應用參考:瀏覽器工作過程及相關名詞 | 部落格園-SRIGT

(3)預載入和預讀取

a. 預載入

  • 預載入:一種資源提示,它告訴瀏覽器這個資源對於當前導航立即需要,並且應該被優先下載和解析

    • 如字型、CSS 和關鍵 JavaScript 指令碼
  • 使用 <link rel="preload"> 預載入關鍵資源

    <head>
      <!-- 預載入字型 -->
      <link rel="preload" href="fonts/myfont.woff2" as="font" type="font/woff2" crossorigin="anonymous">
    
      <!-- 預載入CSS -->
      <link rel="preload" href="styles/main.css" as="style">
    
      <!-- 其他頭部標籤... -->
    </head>
    

b. 預讀取

  • 預讀取:一種資源提示,它告訴瀏覽器這個資源可能會在將來的導航中被用到,但不像預載入那樣具有緊迫性

    • 瀏覽器可以選擇在空閒時間下載這些資源,以便在使用者實際需要它們時能夠更快地載入
  • 使用 <link rel="prefetch"> 預讀取可能需要的未來資源

    <link rel="prefetch" href="details/product-images.jpg">
    <link rel="prefetch" href="details/product-details.js"> 
    

(4)本地快取

  • 儲存限制:儲存大小限制在 4KB 左右,且儲存數量有限制
  • 缺點:
    1. 每次都會將 Cookie 資料攜帶在 HTTP 請求中,可能帶來效能問題佔用頻寬
    2. 安全性較低
  • 由於瀏覽器的跨域限制,客戶端和服務端必須保證同源原則
  • 場景:跟蹤使用者會話資訊,如使用者登入狀態、購物車資訊等
  • Session:以鍵值對的方式將快取資料儲存在伺服器中,並把鍵值(Session ID)作為 Cookie 返給瀏覽器
  • Token:應對移動網際網路不提供 Cookie 的解決方案,將資料雜湊加密並儲存在移動端的儲存系統中
    • JWT 是一種廣泛應用的 Token 標準

b. LocalStorage

  • HTML5 提供的一種新的本地快取方案,用於儲存資料在使用者的瀏覽器中
  • 儲存限制:
    • 長久儲存,無有效期,直到手動刪除或瀏覽器清理快取為止
    • 儲存空間一般可以達到 5MB 及以上(不同的瀏覽器有所區別)
  • 場景:儲存需要在多個頁面或會話中持久儲存的資料,如使用者偏好設定、遊戲進度等

c. SessionStorage

  • 大體與 LocalStorage 類似,在儲存限制上,資料僅在當前會話期間有效,瀏覽器關閉或標籤頁關閉後資料即被清除
  • 場景:儲存僅在當前會話中需要的資料,如臨時狀態資訊、表單資料等

d. IndexedDB

  • 一個低階的 API,允許進行復雜的查詢、事務處理和資料庫管理操作,提供索引功能
  • 儲存限制:儲存空間相對較大,可以儲存大量資料
  • 場景:儲存大量結構化資料並需要進行復雜查詢,如離線應用、遊戲資料儲存等

e. Cache API

  • 一種用於儲存和檢索網路請求的響應的介面

  • 可以與 Service Workers 結合使用,實現離線應用和效能最佳化

    Service Workers:在 Web 瀏覽器中執行的指令碼,具備在後臺獨立於網頁執行的能力

    • 提供很多高階功能,如離線內容快取、推送通知、背景資料同步等
  • 場景:精確控制快取策略和資源快取,如構建 PWA(Progressive Web Apps)時

0x05 框架與庫的選擇與最佳化

(1)框架

  • 輕量級

    • Preact:3kb 大小的 React 輕量、快速替代方案,擁有相同的現代 API
    • Vue3:在 Vue2 基礎上,應用 Tree-shaking,允許在構建過程中自動移除未使用的程式碼
  • 按需載入

    • 採用程式碼分割懶載入技術,將應用拆分成多個小塊,並在需要時才載入它們

    • 以 Vue Router 為例,Vue Router 支援動態匯入

      const routes = [
        {
          path: "/products",
          name: "Products",
          // 使用動態匯入來懶載入元件
          component: () =>
            import(/* webpackChunkName: "products" */ "./views/Products.vue"),
        },
        // 其他路由...
      ];
      

(2)第三方庫

  • 僅引入必要的庫,避免過時或冗餘的庫,定期檢查更新以利用效能最佳化

0x06 效能監控與最佳化工具

(1)效能分析工具

  • Lighthouse:Google 開發的一款開源自動化工具,整合在 Chrome DevTools 中
  • PageSpeed Insights:Google 提供的一款免費工具,官網連結
  • Chrome DevTools Performance 皮膚:整合在 Chrome DevTools 中

(2)使用者效能監控

採用整合 RUM(Real User Monitoring)工具收集實際使用者的載入效能資料

  • Google Analytics:使用外掛 Google Tag Manager 來部署 RUM 指令碼,該指令碼將收集使用者互動資料併傳送到 Google Analytics 進行處理和分析
  • SpeedCurve:https://www.speedcurve.com/

0x07 其他最佳化策略

(1)首屏內容最佳化

  • 確保首屏載入時立即展示關鍵內容,避免使用者看到空白或載入指示器過久
  • 舉例:電商網站首屏最佳化操作
    • 精簡首屏內容:保留最關鍵的內容,如網站Logo、歡迎語、輪播圖、商品推薦
    • 最佳化圖片和指令碼:壓縮和最佳化所有首屏載入的圖片,合併和壓縮 CSS 和 JavaScript 指令碼進行,減少HTTP請求次數
    • 非同步載入非關鍵內容:非首屏關鍵內容設定為非同步載入,即使用者滾動到相應位置時再載入這些內容
    • 使用CDN加速:將網站內容分發到全球多個 CDN 節點,根據使用者地理位置選擇最近的節點進行載入
    • 預載入和快取:利用瀏覽器的預載入和快取機制,提前載入和儲存一些常用的資原始檔

(2)語義化 HTML

  • 使用合理 HTML 標記以及其特有的屬性去格式化文件內容,提高內容可理解性

  • 詳細方法參考:HTML語義化 | CSDN-北航程式設計師小陳

  • 舉例:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>新聞文章標題</title>
    </head>
    
    <body>
      <header>
        <h1>新聞文章標題</h1>
        <p>由 <a href="/author-profile">作者姓名</a> 發表於 <time datetime="2023-04-01">2023年4月1日</time></p>
      </header>
    
      <main>
        <article>
          <h2>引言</h2>
          <p>這裡是引言部分的內容,簡要介紹文章的主題和背景。</p>
    
          <h2>正文標題</h2>
          <p>這裡是正文的第一段,詳細闡述文章的主要觀點或故事。</p>
    
          <h3>小節標題</h3>
          <p>這裡是文章中的一個小節,進一步細化或支援主要觀點。</p>
    
          <!-- 可以繼續新增更多的h2、h3、p等元素來構建文章內容 -->
    
          <footer>
            <p>文章結束。</p>
          </footer>
        </article>
      </main>
    
      <aside>
        <h2>相關文章</h2>
        <ul>
          <li><a href="/article1">相關文章1</a></li>
          <li><a href="/article2">相關文章2</a></li>
          <!-- 更多相關文章連結 -->
        </ul>
      </aside>
    
      <footer>
        <p>版權所有 &copy; 2023 網站名稱</p>
      </footer>
    </body>
    
    </html>
    

(3)後設資料

  • 設定 <title><meta><link rel="canonical"> 等 SEO 相關標籤

  • 舉例:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <!-- 頁面編碼,用於處理不同語言的字串 -->
      <meta charset="UTF-8" />
    
      <!-- 設定視口,確保網頁在不同裝置上正確顯示 -->
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
      <!-- 頁面標題,顯示在瀏覽器標籤和搜尋結果中 -->
      <title>頁面標題 - 網站名稱</title>
    
      <!-- 頁面描述,顯示在搜尋結果中,用於概括頁面內容 -->
      <meta name="description" content="這裡是頁面的簡短描述,應包含關鍵詞並吸引使用者點選。" />
    
      <!-- 頁面關鍵詞,雖然現代搜尋引擎對keywords標籤的重視程度降低,但仍可作為參考 -->
      <meta name="keywords" content="關鍵詞1, 關鍵詞2, 關鍵詞3" />
    
      <!-- 指定頁面的規範URL,有助於防止內容重複被搜尋引擎索引 -->
      <link rel="canonical" href="https://www.example.com/your-page-url" />
    
      <!-- 其他可能的後設資料 -->
      <meta name="author" content="作者姓名或組織" /> <!-- 新增作者資訊 -->
      <meta name="robots" content="index, follow" /> <!-- 指示搜尋引擎索引並跟蹤頁面上的連結 -->
    
      <!-- 對於響應式網站,可以使用meta標籤來適應不同裝置 -->
      <meta name="HandheldFriendly" content="true" /> <!-- 告訴移動裝置,頁面適合於手機瀏覽 -->
      <meta name="MobileOptimized" content="320" /> <!-- 指定移動裝置的螢幕寬度,以適應響應式設計 -->
    
      <!-- 引入CSS樣式 -->
      <link rel="stylesheet" href="style.css" />
    </head>
    
    <body>
      <!-- 頁面內容 -->
      <!-- 引入JavaScript指令碼 -->
      <script src="script.js"></script>
    </body>
    
    </html>
    

(4)結構化資料

  • 結構化資料:一種使用特定格式(如 JSON-LD、Microdata 或 RDFa)來標記網頁內容的方式,以便搜尋引擎和其他機器能夠更容易地理解和處理這些資訊

  • 舉例:以下是採用 Schema.org 和JSON-LD 格式的結構化資料

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <meta charset="UTF-8" />
      <title>電影《星際穿越》</title>
      <script type="application/ld+json">
        {  
          "@context": "https://schema.org/",  
          "@type": "Movie",  
          "name": "星際穿越",  
          "image": "https://example.com/movie-poster.jpg",  
          "director": {  
            "@type": "Person",  
            "name": "克里斯托弗·諾蘭"  
          },  
          "genre": ["科幻", "劇情", "冒險"],  
          "actor": [  
            {  
              "@type": "Person",  
              "name": "馬修·麥康納希"  
            },  
            {  
              "@type": "Person",  
              "name": "安妮·海瑟薇"  
            }  
          ],  
          "datePublished": "2014-11-07",  
          "description": "一隊探險家利用他們針對蟲洞的新發現,超越人類對於太空旅行的極限,從而開始在廣袤的宇宙中進行星際航行的故事。",  
          "aggregateRating": {  
            "@type": "AggregateRating",  
            "ratingValue": "8.7",  
            "reviewCount": "123456"  
          }  
        }  
        </script>
    </head>
    
    <body>
      <!-- 網頁內容 -->
      <h1>電影《星際穿越》</h1>
      <p>導演:克里斯托弗·諾蘭</p>
      <p>主演:馬修·麥康納希, 安妮·海瑟薇</p>
      <p>型別:科幻, 劇情, 冒險</p>
      <p>上映日期: 2014年11月7日</p>
      <p>劇情簡介:...(詳細描述)</p>
    </body>
    
    </html>
    

(5)無障礙性(a11y)

  • 無障礙性:確保網站對所有使用者,包括殘障使用者,都能夠友好地訪問和使用
  • WCAG 標準:Web 內容無障礙指南
  • 舉例:
    1. 非文字內容的文字替代
      • 如:<img alt="這是一張圖片" />
    2. 鍵盤可訪問
    3. 清晰和一致的導航
    4. 足夠的顏色對比度
    5. 字幕和音訊描述

-End-

相關文章