專案不知道如何做效能優化?不妨試一下程式碼分割

冴羽發表於2020-03-24

前言

本篇的作者是來自淘系技術部使用者增長前端團隊的會考古懂金融的資深前端 “空堂”。

最近我們在校招前端面試衝刺互助群裡輔導簡歷的時候,經常有同學感嘆不知道怎麼優化專案,大家不妨嘗試下在專案中引入程式碼分割的方式提升效能。

一、Web 應用效能優化的關鍵

關於 Web 應用效能優化,有一點是毫無疑問的:「頁面載入越久,使用者體驗就越差」。我們幾乎可以說 Web 應用效能優化的關鍵之處就在於:減少頁面初載時,所需載入資源的「數量」和「體積」

那麼當所需載入的資源數量到達多少或資源大小小於多少,我們才可以自信地宣稱我們的 Web 應用擁有出色的效能呢?

下面是我給出的一個參考值,該參考值考慮到了移動端與國外等多種訪問環境:

  1. 頁面初載時,所有未壓縮的 JavaScript 指令碼大小:<=200KB
  2. 頁面初載時,所有未壓縮的 CSS 資源大小:<=100KB
  3. HTTP 協議下,請求資源數:<=6 個
  4. HTTP/2 協議下,請求資源數:<=20 個
  5. **90%**的程式碼利用率(也就是說,僅允許 10% 的未使用程式碼);

或許你會覺得這個標準有點過於苛刻了,是有一點點,但為了建立出高效能的 Web 應用,你的實際資源載入情況應該儘可能靠近這個目標。

二、如何檢視程式碼利用率

也許你注意到了,我們上一節最後提到的一個指標是「程式碼利用率」,你可能是第一次聽說這個概念,這裡我解釋一下它的計算方式:

程式碼利用率 = 你頁面中實際被執行的程式碼 / 你頁面中引入的程式碼 * 100%

你可能會困惑在實際開發中如何得到這個值,別擔心,通過使用 Chrome 開發者工具(很遺憾,目前只有 Chrome 支援這一功能),你就可以迅速對你的 Web 應用進行分析,得到當前頁面下的程式碼利用率狀態,步驟如下:

  1. 開啟 Chrome Dev Tool;
  2. 按下 Cmd + Shift + P or Ctrl + Shift + P ;
  3. 輸入 Coverage,並選擇第一個出現的選項;

圖 1:直接輸入 coverage 即可開啟該看板

  1. 點選皮膚上的 reload 按鈕,檢視整個應用 JavaScript 的程式碼利用率;

圖 2:點選 reload 按鈕,開始分析 JavaScript 程式碼利用率

三、提高程式碼使用率的關鍵技術 — 程式碼分割(code splitting)

什麼是「程式碼分割」(code splitting)?

程式碼分割是指,將指令碼中無需立即呼叫的程式碼在程式碼構建時轉變為非同步載入的過程。

在 Webpack 構建時,會避免載入已宣告要非同步載入的程式碼,非同步程式碼會被單獨分離出一個檔案,當程式碼實際呼叫時被載入至頁面。

程式碼分割的原理

程式碼分割技術的核心是「非同步載入資源」,可喜的是,瀏覽器允許我們這麼做,W3C stage 3 規範: whatwg/loader 對其進行了定義:你可以通過 import() 關鍵字讓瀏覽器在程式執行時非同步載入相關資源。

import() 的瀏覽器支援性

沒錯,正如你所看到的, IE 瀏覽器目前並不支援這一特性,但這並不意味著你的非同步載入功能在 IE 瀏覽會失效(那太可怕了 ?‍♂️),實際上,Webpack 底層幫你將非同步載入的程式碼抽離成一份新的檔案,並在你需要時通過 JSONP 的方式去獲取檔案資源,因此,你可以在任何瀏覽器上實現程式碼的非同步載入,並且在將來所有瀏覽器都實現 import() 方法時平滑過渡,cool!?

程式碼分割的型別

程式碼分割可以分為「靜態分割」和「“動態分割」兩種方式,注意這裡打了引號的 “動態”,因為實際上它並不意味著非同步呼叫的程式碼是 “動態” 生成的,我們之後會看到 Webpack 是如何做到這一點的,在那之前,讓我們先看看「靜態程式碼分割」。

靜態程式碼分割

靜態程式碼分割是指:在程式碼中明確宣告需要非同步載入的程式碼。

下面 ? 的程式碼說明了我們應該如何使用這一技術:

import Listener from './listeners.js'
const getModal = () => import('./src/modal.js') Listener.on('didSomethingToWarrentModalBeingLoaded', () => {  // Async fetching modal code from a separate chunk  getModal().then((module) => {    const modalTarget = document.getElementById('Modal')    module.initModal(modalTarget)  })})
const getModal = () => import('./src/modal.js') 
Listener.on(
  'didSomethingToWarrentModalBeingLoaded',
  () => {
    // Async fetching modal code from a separate chunk
    getModal().then(
      (module) => {
        const modalTarget = document.getElementById('Modal')    
        module.initModal(modalTarget)  
      })
  }
)
複製程式碼

正如你所看到的:每當你呼叫一個宣告瞭非同步載入程式碼的變數時,它總是返回一個 Promise 物件。

⚠️ 注意:在 Vue 中,可以直接使用 import() 關鍵字做到這一點,而在 React 中,你需要使用 react-loadable 去完成同樣的事。

最後,讓我們談談何時使用靜態程式碼分割技術,這一技術適合以下的場景:

1. 你正在使用一個非常大的庫或框架:如果在頁面初始化時你不需要使用它,就不要在頁面初載時載入它; 2. 任何臨時的資源:指不在頁面初始化時被使用,被使用後又會立即被銷燬的資源,例如模態框,對話方塊,tooltip 等(任何一開始不顯示在頁面上的東西都可以有條件的載入); 3. 路由:既然使用者不會一下子看到所有頁面,那麼只把當前頁面相關資源給使用者就是個明智的做法; 好了,現在你掌握了靜態程式碼分割技術,現在讓我們看看什麼是「“動態代”程式碼分割」技術。

動態程式碼分割

動態程式碼分割是指:

在程式碼呼叫時根據當前的狀態,「動態地」非同步載入對應的程式碼塊。

下面 ? 的程式碼說明了它具體是如何被實現的:

const getTheme = (themeName) => import(`./src/themes/${themeName}`)
// using `import()` 'dynamically'
if (window.feeling.stylish) {
  getTheme('stylish').then((module) => {
    module.applyTheme()
  })
} else if (window.feeling.trendy) {
  getTheme('trendy').then((module) => {
    module.applyTheme()
  })
}
複製程式碼

看到了嗎,我們 “動態” 的宣告瞭我們要非同步載入的程式碼塊,這是怎麼做到的?!答案出乎意料的簡單,Webpack 會在構建時將你宣告的目錄下的所有可能分離的程式碼都抽象為一個檔案(這被稱為 contextModule 模組),因此無論你最終宣告瞭呼叫哪個檔案,本質上就和靜態程式碼分割一樣,在請求一個早已準備好的,靜態的檔案。

下面是一些使用 “動態” 程式碼分割技術的場景:

1. A/B Test:你不需要在程式碼中引入不需要的 UI 程式碼; 2. 載入主題:根據使用者的設定,動態載入相應的主題; 3. 為了方便 :本質上,你可以用靜態程式碼分割代替「動態」程式碼分割,但是後者比前者擁有更少的程式碼量;

四、魔術註釋

魔術註釋是由 Webpack 提供的,可以為程式碼分割服務的一種技術。通過在 import 關鍵字後的括號中使用指定註釋,我們可以對程式碼分割後的 chunk 有更多的控制權,讓我們看一個例子:

// index.js
import (
  /* webpackChunkName: “my-chunk-name” */
  './footer'
)
複製程式碼

同時,也要在 webpack.config.js 中做一些改動:

// webpack.config.js
{
  output: {
    filename: “bundle.js”,
    chunkFilename: “[name].lazy-chunk.js”
  }
}
複製程式碼

通過這樣的配置,我們可以對分離出的 chunk 進行命名,這對於我們 debug 而言非常方便。

Webpack Modes

除了上面提到過得 webpackChunkName 註釋外,Webpack 還提供了一些其他註釋讓我們能夠對非同步載入模組擁有更多控制權,例如下方這個例子:

import (
  /* webpackChunkName: “my-chunk-name” */
  /* webpackMode: lazy */
  './someModule'
)
複製程式碼

webpackMode 的預設值為 lazy 它會使所有非同步模組都會被單獨抽離成單一的 chunk,若設定該值為 lazy-once,Webpack 就會將所有帶有標記的非同步載入模組放在同一個 chunk 中。

Prefetch or Preload

通過新增 webpackPrefetch 魔術註釋,Webpack 令我們可以使用與 <link rel=“prefetch”> 相同的特性。讓瀏覽器會在 Idle 狀態時預先幫我們載入所需的資源,善用這個技術可以使我們的應用互動變得更加流暢。

import(
  /* webpackPrefetch: true */
  './someModule'
)
複製程式碼

⚠️ 注意:你確保你的程式碼在未來一定會用到時,再開啟該功能。

五、小結

至此,我們講解了所有有關 Code Splitting 的知識,並告訴你了一些神奇的「魔法註釋」讓你對分割後的程式碼有更多的掌控,希望你能將上面的技術靈活運用在你的專案中,開發出更加激動人心,如絲般順滑的應用! Good Luck!?

其他

建立了一個「前端校招面試衝刺互助群」,歡迎加「taoxiaozhao233」 入群~

相關文章