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

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

在 2019 年 Google I/O 大會 Mariko, Jake 和我發行的 PROXX 是用於網路的新型 Minesweeper 複製作業。PROXX 之所以能脫穎而出,是因為具有無障礙設計 (您可以使用螢幕閱讀器播放!),以及在功能型手機上和高階電腦裝置上運作的能力。功能手機受到多種限制的影響:

  • CPU 使用率偏低
  • GPU 強度不足或不存在
  • 不支援觸控輸入的小螢幕
  • 記憶體容量非常有限

但他們執行的是新世代瀏覽器,價格也相當親民。因此,功能型手機在新興市場越來越受歡迎。他們的價位提供過去無法負擔的全新目標對象,讓他們得以重新在網路上運用現代化網路。根據預測,光是 2019 年,光是在印度銷售大約 4 億款功能型手機,因此使用功能手機的使用者可能成為您的大量目標對象。再加上 2G 這樣的連線速度,是新興市場的常態。我們如何管理 PROXX 在功能型手機條件下順利運作?

PROXX 遊戲過程。

載入效能和執行階段效能都至關重要,而且效能非常重要。研究結果顯示,良好的成效與使用者留存率、轉換率提高有關,最重要的是,多元包容性會提高。Jeremy Wagner成效的重要性方面有更多資料和洞察資訊。

本系列分為兩部分,此為第一部分。第 1 部分著重於載入效能,第 2 部分將著重說明執行階段效能。

掌握現狀

請務必在「實際」裝置上測試載入效能。如果您沒有實體裝置,建議您使用 WebPageTest,特別是「simple」設定WPT 會在使用 3G 模擬連線的「實際」裝置上執行載入測試的電池。

3G 是不錯的測量速度。儘管你可能習慣 4G、LTE,或是很快就有 5G 網路,但行動網路的現實情況卻大不相同。例如在火車、會議、音樂會或飛機上。但遇到的情況可能非常接近 3G,有時甚至更糟。

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

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

透過 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 必須在載入這些匯入項目後才能執行。在這裡需要注意的是,在建構期間就知道這些依附元件類型。我們可以使用 <link rel="preload"> 標記,確保所有依附元件在我們收到的 HTML 後即可開始載入。

結果

我們來看看變更後的成效。請務必避免變更測試設定中的任何其他變數,導致結果產生偏差,因此我們將使用 WebPageTest 的簡易設定處理本文的其他部分,並查看幻燈片:

我們利用 WebPageTest 的幻燈片瞭解變更的效果。

這些調整讓 TTI 從 11 降至 8.5 分,約為 2.5 秒的連線設定時間。非常好!

預先算繪

雖然我們剛才減少了 TTI,但使用者必須終於連續 8.5 秒才能維持長時間的白色畫面。只要在 index.html 中傳送樣式標記,FMP 就能發揮最大的效益。常見的做法是預先轉譯和伺服器端轉譯,兩者密切相關,詳情請參閱「在網路上轉譯」。這兩種技術都會在 Node 中執行網頁應用程式,並將產生的 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 秒時出現,因此基本上不會受到這項異動影響。我們這麼做的方式是認知的改變。有些人甚至稱之為手。藉由算繪遊戲的中間視覺,我們改善了感知的載入效能。

內嵌

開發人員工具和 WebPageTest 提供的另一項指標是首次位元組 (TTFB)。這是從要求的第一個位元組傳送到所收到回應的第一個位元組所需要的時間。這個時間通常也稱為「封包往返時間 (RTT),但嚴格來說,這兩個數字會有所差異:RTT 不包含伺服器端的要求處理時間。在要求/回應區塊中,DevTools 和 WebPageTest 能以淺色視覺化 TTFB。

要求的淺色部分代表該要求正在等待接收回應的第一個位元組。

查看我們的刊登序列後,我們發現所有要求都花費大部分時間在等候回應的第一個位元組

這就是最初所設的 HTTP/2 Push 所會發生的問題。應用程式開發人員知道需要特定資源,並能「推送」這些資源,當用戶端發現需要擷取其他資源時,這些項目就已存放在瀏覽器的快取中。由於 HTTP/2 推送難以正確取得,因此不建議使用。標準化 HTTP/3 期間會重新檢查這個問題空間。目前,最簡單的解決方法是內嵌所有重要資源,但卻不會產生快取效率。

CSS 模組和以 Puppeteer 為基礎的預先轉譯器,已經將我們重要的 CSS 導入中。如果是 JavaScript,我們必須內嵌重要模組及其依附元件。根據您使用的整合工具,這項工作的難度各有不同。

,瞭解如何調查及移除這項存取權。
透過 JavaScript 的內嵌,我們將 TTI 從 8.5 秒降低為 7.2 秒。

這隻刮鬍子從 TTI 大放了 1 秒。現在,index.html 包含初始轉譯和互動性啟用所需的所有功能。HTML 可以在下載期間進行轉譯,建立 FMP。HTML 剖析完成並執行時,應用程式可以互動。

積極分割程式碼

是,我們的 index.html 包含所有具備互動功能的必要項目。但仔細檢查後,結果發現還包含其他所有東西。我們的 index.html 約為 43 KB。這意味著使用者可在一開始可進行互動:我們使用表單來設定遊戲,其中包含幾個元件、啟動按鈕,以及一些用於保存使用者設定的程式碼。就是這麼簡單。43 KB 好像很多。

PROXX 的到達網頁。這裡只使用重要元件。

如要瞭解套件大小的來源,可以使用來源地圖探索工具或類似工具來細分組合包含的項目。如預測所示,我們的套裝組合包含遊戲邏輯、轉譯引擎、贏得畫面、遺失畫面和許多公用程式。到達網頁只需要一小部分模組。將互動時非必要的所有項目移至延遲載入的模組中,可大幅降低 TTI。

分析 PROXX 的 `index.html` 內容會顯示許多不需要的資源。醒目顯示重要資源。

第一個步驟是「程式碼分割」。程式碼分割功能會將單體式套件分為多個小部分,方便隨選延遲載入。WebpackRollupParcel 等熱門套裝組合工具都支援使用動態 import() 分割程式碼。套裝組合會分析您的程式碼,並將所有以靜態方式匯入的模組內嵌。您動態匯入的所有項目都會放在各自的檔案中,且在執行 import() 呼叫後,才會從網路擷取。當然,抵達聯播網會需要費用,因此除非有足夠的時間,否則不建議這麼做。重點在於靜態匯入載入時間「至關重要」所需的模組,然後動態載入所有其他項目。不過,請不要等到最後一刻才有一定會會用到的延遲載入模組。Phil WaltonIdle Until Urgent 是能在延遲載入與熱力載入之間維持健康中間地的理想模式。

在 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() 函式中使用元件的 Promise。舉例來說,用於算繪動畫背景圖片的 <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 秒。與原始 11 相比,效能大幅提升。

我們的 FMP 和 TTI 相距 100 毫秒,因為這只涉及剖析和執行內嵌 JavaScript。使用 2G 網路的 5.4 秒之後,這款應用程式就能與人互動。至於其他較不重要的模組,則會在背景載入。

觸手可及

如查看上方的重要模組清單,就會發現轉譯引擎並非重要模組的一部分。當然,在沒有算繪引擎算繪遊戲之前,遊戲無法啟動。我們可以停用「Start」來啟動顯示引擎,但事實上,使用者通常不需要花太多時間來設定遊戲設定。大多數情況下,轉譯引擎和其他其餘模組都是在使用者按下「Start」後才完成載入。在極少數的情況下,如果使用者比網路連線快,我們會顯示簡單的載入畫面,等待其餘模組完成。

結論

評估相當重要。為避免將時間花在真正的問題上,我們建議在執行最佳化之前,一律先評估。此外,請使用 3G 連線的實際裝置進行測量;如果沒有實體裝置,請使用 WebPageTest 進行測量。

幻燈片可讓您深入瞭解對使用者載入應用程式的過程。你可以根據瀑布圖,找出哪些資源導致載入時間過長。以下提供一份清單,列出提高載入效能的方法:

  • 透過單一連線盡量提供最多素材資源。
  • 預先載入第一個轉譯和互動所需的內嵌資源,或甚至內嵌資源。
  • 預先轉譯應用程式,改善感知載入效能。
  • 使用積極的程式碼分割功能,減少互動所需的程式碼量。

我們將在第 2 季中探討如何在超受限裝置上最佳化執行階段效能,敬請密切關注。