加快網頁應用程式載入速度的技巧,就算是功能型手機也能快速載入

我們如何在 PROXX 中使用程式碼分割、程式碼內嵌和伺服器端轉譯功能。

在 2019 年 Google I/O 大會的 Mariko、Jake 和我出貨的 PROXX,這是一個專為網路設計的現代礦物複製人。PROXX 與眾不同之處在於,無障礙設計 (可透過螢幕閱讀器播放!) 和功能型手機都能在高階電腦裝置上運作。功能手機受到多種限制:

  • 低 CPU 使用率
  • GPU 弱或不存在
  • 不需觸控輸入的小型螢幕
  • 記憶體容量有限

不過,這款應用程式搭載新式瀏覽器,而且價格實惠。有鑑於此,功能型手機在新興市場也大受歡迎。他們的價格點出許多先前無法負擔的新讀者,他們可以上網並運用現代網路。2019 年,預計光是印度將有約 4 億部功能手機販售,因此使用功能型手機的使用者可能會成為大部分目標對象。除此之外,新興市場的連線速度也比 2G 來得低。我們如何確保 PROXX 在功能型手機條件下正常運作?

PROXX 遊戲體驗。

效能十分重要,包含載入效能和執行階段效能。研究顯示,成效提升與使用者留存率、轉換率提高,最重要的是,多元包容性上升Jeremy Wagner 提供更多資料和深入分析,讓你瞭解效能的重要性

本文是兩部分系列中第 1 部分的內容。第 1 部分著重於載入效能,第 2 部分將著重於執行階段效能。

掌握現況

您必須在「實際」裝置上測試載入效能,如果您手邊沒有實際裝置,建議您使用 WebPageTest,尤其是「簡單」設定WPT 在具備模擬 3G 連線的「實際」裝置上執行 WPT 載入測試。

3G 是很好的測量速度。使用者可能習慣使用 4G、LTE 或甚至是 5G 網路,但現在行動網際網路的發展情況截然不同。無論是搭火車、參加會議、參加演唱會,還是在航班中,您會遇到的情況很可能發生在 3G 連線更近,有時甚至更糟。

儘管如此,由於 PROXX 會明確定位到功能型手機和新興市場,因此本文將著重在 2G 網路。WebPageTest 執行測試後,畫面上會顯示瀑布 (類似於開發人員工具中的畫面) 以及頂端的幻燈片。影片膠卷會顯示使用者在載入應用程式時看到的內容。在 2G 上,未最佳化版本的 PROXX 載入體驗不佳:

幻燈片影片會顯示 PROXX 透過模擬的 2G 連線,在真實的低階裝置上載入時,使用者所看到的內容。

透過 3G 網路載入時,使用者會看到 4 秒的白色內容。透過 2G 網路,使用者看了 8 秒時絕對看不到任何內容。閱讀成效的重要性一文後,您知道我們現在已因衝刺而錯失可觀的潛在使用者。使用者必須下載全部 62 KB 的 JavaScript,畫面上才會顯示所有內容。在這個案例中,您看到的第二項是互動式內容。或是說有可能呢?

在 PROXX 未最佳化版本中,[首次有效繪製][FMP] 具備「技術性」_[互動式][TTI],但對使用者而言毫無用處。

下載約 62 KB 的 gzip JS 並產生 DOM 後,使用者就能查看我們的應用程式。這款應用程式具備「技術性」的互動功能。然而,以視覺呈現卻有截然不同的效果。網路字型仍在背景中載入,直到內容就緒後,使用者看不到任何文字。雖然這個狀態符合「首次有效繪製 (FMP)」的資格條件,但不一定符合適當的「互動」條件,因為使用者無法判斷任何輸入內容的內容為何。啟用 3G 網路後,裝置只需幾秒;透過 2G 網路則需要 3 秒,直到應用程式準備就緒。換句話說,這款應用程式在 3G 連線上只需 6 秒,在 2G 連線為 11 秒,才能啟動互動模式。

瀑布分析

現在我們已瞭解使用者會看到的「內容」,接著需要找出「原因」。為此,我們可以查看瀑布圖,並分析為何資源載入時間太晚。在 PROXX 的 2G 追蹤記錄中,您會看到兩種主要的紅色旗標:

  1. 有多個彩色細線。
  2. JavaScript 檔案構成鏈結。舉例來說,第二項資源只有在第一個資源完成後才會開始載入,而第三項資源只會在第二個資源結束時啟動。
刊登序列會深入分析載入的資源時間和所需時間。

減少連線數量

每個細行 (dnsconnectssl) 都代表建立新的 HTTP 連線。透過 3G 連線大約需要 1 個網路,使用 2G 服務大約需要 2.5 秒,設定新連線的費用也很高。在瀑布中,我們發現下列主題新增了一項連結:

  • 要求 #1:我們的index.html
  • 要求 #5:fonts.googleapis.com 的字型樣式
  • 要求 8:Google Analytics (分析)
  • 要求 9:fonts.gstatic.com 提供的字型檔案
  • 要求 #14:網頁應用程式資訊清單

index.html」的新連線不可避免。瀏覽器「必須」與我們的伺服器建立連線,才能取得內容。內嵌 Minimal Analytics 等內容可以避免新連結與 Google Analytics (分析) 連結,但 Google Analytics (分析) 不會阻止應用程式顯示或產生互動,所以我們不在乎載入速度。在理想情況下,Google Analytics (分析) 應在閒置時間載入,然後已載入其他項目。這樣一來,在初始載入期間,就不會佔用頻寬或處理效能。系統會根據擷取規格要求新的網頁應用程式資訊清單連線,因為必須透過非憑證連線載入資訊清單。同樣地,網頁應用程式資訊清單不會妨礙應用程式顯示內容或參與互動,因此我們並不考慮這點。

然而,這兩個字型和樣式卻會阻礙轉譯,且會妨礙互動性。假如我們查看由 fonts.googleapis.com 傳送的 CSS,那隻是兩個 @font-face 規則,每種字型各有一個。事實上,字型樣式太小,我們決定將它內嵌到 HTML 中,並移除不必要的連結。如要避免為字型檔案的連線設定成本,我們可以將其複製到我們的伺服器。

平行處理載入作業

從瀑布圖中看到,第一個 JavaScript 檔案載入完畢後,新檔案會立即開始載入。這是模組依附元件的常見情況。我們的主模組可能採用靜態匯入,因此必須先載入 JavaScript,才能執行 JavaScript。請務必留意,在建構時已知這些依附元件類型。我們可以使用 <link rel="preload"> 標記,確保所有依附元件都會開始載入我們收到 HTML 的第二個。

結果

讓我們來看看變更結果。請務必不要變更測試設定中任何其他可能會使結果出現偏差的變數。因此,我們將針對本文的其餘部分使用 WebPageTest 簡易的設定,查看幻燈片:

我們利用 WebPageTest 的幻燈片查看變更項目。

這些異動使 TTI 從 11 秒縮短至 8.5 秒,大約需要 2.5 秒的連線設定時間。我們做得好,

預先算繪

雖然我們剛減少了 TTI,但也不會影響使用者長時間等候 8.5 秒鐘的白色螢幕。您應該index.html 中傳送樣式化標記,對 FMP 的最大改善幅度。要達成上述目標的常見技術是預先算繪和伺服器端轉譯,這一點密切相關,相關說明請參閱「在網路上轉譯」。這兩種技術都會在節點中執行網頁應用程式,並將產生的 DOM 序列化為 HTML。伺服器端轉譯也會依伺服器端的要求進行這項作業,而預先算繪功能則會在建構期間進行這項作業,並將輸出內容儲存為新的 index.html。由於 PROXX 是 JAMStack 應用程式,沒有伺服器端,因此我們決定導入預先算繪。

實作預先轉譯器的方法有很多種。在 PROXX,我們選擇使用 Puppeteer,這個方式可在沒有使用者介面的情況下啟動 Chrome,並可讓您透過 Node API 遠端控制該執行個體。我們使用它插入標記和 JavaScript,然後將 DOM 讀取為 HTML 字串。我們採用 CSS 模組,因此可以免費取得所需的 CSS 樣式。

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(rawIndexHTML);
  await page.evaluate(codeToRun);
  const renderedHTML = await page.content();
  browser.close();
  await writeFile("index.html", renderedHTML);

這樣我們應該可以改善 FMP。我們仍需要像過去一樣載入和執行 JavaScript 大小,因此不會預期 TTI 會大幅改變。而 index.html 變得越來越大,可能會稍微推遲 TTI。只有一個方法可以得知:執行 WebPageTest。

幻燈片顯示 FMP 指標明顯提升的幅度。TTI 大致上不受影響。

我們的第一個有意義的繪製內容已從 8.5 秒變更為 4.9 秒,這是一項大規模的改善。TTI 仍然在 8.5 秒左右發生,因此絕大部分不會受到此次異動的影響。做法是永久的改變。有些人甚至稱作此類。藉由算繪遊戲的中繼圖像,我們改變了使用者感受到的載入效能,藉此提供更好的服務。

內嵌

DevTools 和 WebPageTest 提供的另一個指標是 Time To First Byte (TTFB)。這是指從傳送要求的第一個位元組到收到回應的第一個位元組所花費的時間。這個時間通常也稱為 RTT,不過技術上這兩個數字之間存在差異:RTT 不包含伺服器端要求的處理時間。DevTools 和 WebPageTest 在要求/回應區塊內以淺色顯示 TTFB。

要求的指示燈部分代表要求正在等待接收回應的第一個位元組。

從我們的瀑布圖可以看出,所有要求都花在等待回應的第一個位元組送達的大部分

這是原本系統認定的 HTTP/2 推送問題。應用程式開發人員知道需要特定資源,因此可以將這些資源「推送」。當用戶端發現需要擷取其他資源時,已在瀏覽器的快取中。HTTP/2 推播式攻擊太難取得正確率,因此不建議採用。HTTP/3 的標準化期間,系統會重新造訪這個問題空間。目前,在降低快取效率的情況下,最簡單的解決方案就是內嵌所有重要資源

多虧有 CSS 模組和以 Puppeteer 為基礎的預先轉譯器,我們的重要 CSS 已內嵌於中。針對 JavaScript,我們需要內嵌我們的關鍵模組及其依附元件。視您使用的 Bundler 而定,這項工作的難度會有所不同。

導入 JavaScript 之後,我們將 TTI 從 8.5 秒縮減為 7.2 秒。

這艘太空船廢墟 1 秒鐘了,目前 index.html 包含初始算繪及成為互動項目所需要的所有內容。HTML 可在下載期間進行轉譯,以建立 FMP。HTML 完成剖析和執行時,應用程式屬於互動式應用程式。

積極程式碼分割

是的,我們的 index.html 包含成為互動所需的一切資訊。但仔細檢測後,發現它也包含其他所有東西。我們的 index.html 大小約為 43 KB。我們來說明,使用者一開始可以互動的內容:我們會利用表單設定遊戲,當中包含幾個元件、開始按鈕和一些用來保存及載入使用者設定的程式碼。就差不多了。43 KB 的內容似乎很大。

PROXX 的到達網頁。此處只能使用關鍵元件。

如要瞭解套件大小的來源,我們可以使用來源地圖探索工具或類似工具細分組合元件。如同預測,套件內含遊戲邏輯、轉譯引擎、獲勝畫面、遺失螢幕和許多公用程式。到達網頁只需要少數模組的一部分。將互動性不需要的內容全部移至延遲載入的模組中,將會大幅減少 TTI。

分析 PROXX「index.html」的內容後,系統會顯示許多不需要的資源。重要資源會醒目顯示。

方法很簡單,只要執行程式碼分割即可。程式碼分割功能可將單體式套件分割為較小的部分,隨選載入。WebpackRollupParcel 等熱門軟體包,支援使用動態 import() 分割程式碼。Bundler 會分析你的程式碼,並內嵌所有以靜態匯入的模組。您動態匯入的所有內容都會放入自己的檔案中,而且只有在執行 import() 呼叫後,才會從網路擷取。當然,使用網路會產生費用,只有在有餘裕時再進行。重點是,以靜態方式匯入在載入時間中「重要」所需的模組,並動態載入其他所有必要的模組。但請不要等到最後才使用一定會使用的延遲載入模組。Phil Walton 的《無比恆等》很適合用來在延遲載入和緊急載入之間保持健康的中間地位。

我們在 PROXX 中建立了 lazy.js 檔案,以靜態方式匯入我們「不需要」的所有項目。在主要檔案中,我們就可以動態匯入 lazy.js。不過,部分「Preact」元件會在 lazy.js 中結束,但由於 Preact 無法處理立即載入的元件,因此其中有一些小工具。因此,我們編寫了小型的 deferred 元件包裝函式,允許在實際元件載入之前,算繪預留位置。

export default function deferred(componentPromise) {
  return class Deferred extends Component {
    constructor(props) {
      super(props);
      this.state = {
        LoadedComponent: undefined
      };
      componentPromise.then(component => {
        this.setState({ LoadedComponent: component });
      });
    }

    render({ loaded, loading }, { LoadedComponent }) {
      if (LoadedComponent) {
        return loaded(LoadedComponent);
      }
      return loading();
    }
  };
}

如此一來,我們就可以在 render() 函式中使用元件的承諾。舉例來說,會算繪動畫背景圖片的 <Nebula> 元件會在元件載入期間替換成空白的 <div>。元件載入完成且可供使用時,<div> 就會替換為實際的元件。

const NebulaDeferred = deferred(
  import("/components/nebula").then(m => m.default)
);

return (
  // ...
  <NebulaDeferred
    loading={() => <div />}
    loaded={Nebula => <Nebula />}
  />
);

達成上述所有目標後,我們將 index.html 縮減為只有 20 KB,低於原始大小的一半。這對 FMP 和 TTI 有何影響?WebPageTest 就會知道!

幻燈片確認:目前的 TTI 已調整為 5.4 秒,相較於原本的 11s,我們帶來大幅的改進。

我們的 FMP 和 TTI 只有 100 毫秒,因為只需要剖析和執行內嵌的 JavaScript。連上 2G 網路只要 5.4 秒,就能完全與應用程式互動。所有其他較少必要模組會在背景載入。

更人性化

檢閱以上重要模組清單時,您會發現轉譯引擎並非重要模組的一部分。當然,在我們成功轉譯遊戲的轉譯引擎之前,遊戲就無法開始。我們可以停用「開始」按鈕,直到轉譯引擎準備開始遊戲為止,但根據我們的經驗,使用者通常花費太多時間進行必要的遊戲設定。大部分情況下,轉譯引擎和其他剩餘的模組會在使用者按下「Start」時完成載入。在極少數情況下,如果使用者比網路連線快更快,我們會顯示簡單的載入畫面,藉此等待其他模組完成。

結論

測量方法非常重要。為避免將時間花在實際的問題上,我們建議您先評估再採行最佳化做法。此外,如果沒有實際裝置,則應在 3G 連線的實際裝置上或 WebPageTest 進行評估。

您可以利用幻燈片,深入瞭解載入應用程式對使用者的「費用」。刊登序列會顯示哪些資源導致載入時間過長。以下是改善載入效能的建議事項:

  • 透過單一連線盡可能提供最多的資產。
  • 首次轉譯和互動所需的預先載入,甚至是內嵌資源。
  • 預先轉譯應用程式,改善感知的載入效能。
  • 利用積極的程式碼分割,減少互動所需的程式碼量。

我們將在第 2 部分繼續探討如何在超受限裝置上最佳化執行階段效能。