適合所有網站的前端優化技巧,值得你收藏!

導讀:本文雖然明指了某個主頁,但是其實是適合所有網站系統前端的優化技巧的。

最近,我們將 Universe.com 主頁的性能提升了十倍以上。在本文中,我們將解析實現這一重大改進的具體技術手段。

但在開始之前,讓我們先對網絡性能的重要意義進行一番論證(博文末尾提供相關案例研究鏈接):

在本篇文章中,我們將簡要介紹以下幾大有助於我們提高頁面性能的主要領域:

這裏再介紹一點我們的情況:我們的主頁由 React(TypeScript)、Phoenix(Elixir)、Puppeteer(headless Chrome)以及 GraphQL API(Ruby on Rails)構建而成。以下爲主頁在移動設備上顯示的效果:

Universe 主頁與瀏覽效果

性能測量

沒有數據作爲支持,一切意見都將毫無意義。

—— W. Edwards Deming

實驗室工具

實驗室工具能夠立足受控環境從預定義的設備及網絡設置中收集數據。利用這些工具,我們能夠輕鬆調試任何性能問題並實現良好的可重複測試。

Lighthouse 就是一款立足本地計算機對 Chrome 內網頁進行審計的出色工具。其能夠提供一系列關於如何提高性能、可訪問性以及搜索引擎優化的實用性提示。下面,我們來看模擬高速 3G 加 4x CPU 場景下的 Lighthouse 性能審計報告:

之前與之後:首屏內容填充(簡稱 FCP)性能實現 10 倍提升

然而,單純使用實驗室工具也會帶來不少弊端:這類工具不一定能準確反映出最終用戶所面臨的設備、網絡、位置以及多種其它現實因素造成的性能瓶頸。正因爲如此,我們才需要配合現場工具進行補充。

現場工具

現場工具允許我們模擬並測量用戶的真實頁面負載。目前有多種服務可幫助大家從實際設備當中獲取真實性能數據:

WebPageTest 報告

渲染

內容的渲染可通過多種方法實現,其中每一種都擁有獨特的優勢與缺點:

客戶端渲染

以前,我們將自己的主頁與 Ember.js 框架一同實現爲採用客戶端渲染方法的單頁面應用。但這種做法的一大問題在於,我們的 Ember.js 應用程序包過大。這意味着在瀏覽器下載 JavaScript 文件並對其進行解析、編譯與執行的過程中,用戶只能對着空白屏幕發呆:

最要命的空白屏幕

因此,我們決定利用 React 重構應用當中的某些部分。

預渲染與服務器端渲染

客戶端渲染應用程序的具體構建——例如採用 React Router DOM,仍然會帶來與 Ember.js 相同的問題。JavaScript 需要佔用大量資源,而且訪問者需要經歷一段首屏內容填充週期才能看到實際內容。

因此在決定使用 React 之後,我們開始嘗試其它潛在的渲染選項,以確保瀏覽器能夠更快地完成內容渲染。

使用 React 時的常規渲染選項

因此,我們打算嘗試一下混合方法,即發揮每一種渲染選項中的獨特優勢。

運行時預渲染

Puppeteer 是一套 Node.js 庫,允許用戶使用 headless Chrome。我們希望嘗試利用 Puppeteer 在運行時當中實現預渲染。這代表着一種有趣的混合方法:利用 Puppeteer 進行服務器端渲染,同時利用 hydration 進行客戶端渲染。感興趣的朋友可以點擊此處查看谷歌提供的關於如何利用 headless 瀏覽器進行服務器端渲染的相關提示。

利用 Puppeteer 對 React 應用程序進行運行時預渲染

這種方法具備以下優勢:

但在採用這種方法的過程中,我們也遇到了一些挑戰:

利用 Puppeteer 的服務器端渲染架構

•    穩定性。對衆多 headless 瀏覽器進行規模伸縮,同時保持進程不致過熱並實現負載均衡絕對是一項高難挑戰。我們嘗試了不同的託管方法,包括在 Kubernetes 集羣內進行自託管,以及利用 AWS Lambda 與 Google Cloud Functions 實現無服務器計算。我們注意到,後一種方法在配合 Puppeteer 時存在一些性能問題:

AWS Lambdas 和 GCP 函數的 Puppeteer 響應時間

在配合 AWS Lambdas 與 GCP Functions 時,Puppeteer 的響應時間結果隨着我們對 Puppeteer 熟悉程度的逐步提升,我們開始對初始方法進行迭代(後文將具體說明)。我們還進行了其它一系列有趣的實驗,希望通過 headless 瀏覽器渲染 PDF。再有,即使不編寫任何代碼,我們也能夠利用 Puppeteer 自動進行端到端測試。而且除了 Chrome 之外,Puppeteer 現在還支持 Firefox 瀏覽器。

混合渲染方法

在運行時中使用 Puppeteer 並非易事。正因爲如此,我們才決定在構建時中加以使用,同時配合一款工具用於在運行時內從服務器端獲取用戶生成的實際內容。很明顯,這款工具必須擁有比 Puppeteer 更強大的穩定性與吞吐能力。

我們決定使用 Elixir 編程語言。Elixir 看起來與 Ruby 非常相似,但運行在 BEAM(Erlang VM)之上。順帶一提,BEAM 專門爲構建高容錯、高穩定性系統而生。

Elixir 採用 Actor 併發模型。每個 “Actor”(即 Elixir 進程)的內存佔用量都非常有限,僅爲 1 到 2 KB。這意味着系統將能夠同時運行成千上萬個獨立的進程。Phoenix 則是一套 Elixir Web 框架,能夠支持高吞吐量,並允許開發者在各個獨立的 Exlixir 進程當中處理各項 HTTP 請求。

我們將上述方法結合起來,充分利用其各自優勢,希望能夠切實滿足自身需求:

Puppeteer 用於實現預渲染,Phoenix 則用於實現服務器端渲染

我們可以繼續構建一款簡單的瀏覽器 React 應用程序,並在無需等待最終用戶設備 JavaScript 處理過程的同時獲得快速初始頁面加載效果。

利用 Puppeteer 建立預渲染架構,利用 Phoenix 進行服務器端渲染,React 則在客戶端上實現 hydration

網絡

內容交付網絡 (CDN)

利用 CDN 可幫助我們實現內容緩存,並加速其在全球範圍內的交付速度。我們選擇了 Fastly.com,其目前處理着全球超過 10% 的請求總量,並得到 GitHub、Stripe、Airbnb 以及 Twitter 等諸多廠商的青睞。

Fastly 允許我們編寫定製化緩存,並可利用 VCL 配置語言建立路由邏輯。下面,我們將具體聊聊基礎請求流如何根據路由、請求頭等因素分步起效:

VCL 請求流

提高性能的另一個選項是配合 Fastly 在邊緣位置使用 WebAssembly(WASM)。大家可以將其視爲一種無服務器模式,只是處於邊緣位置;所使用的語言則包括 C、Rust、Go 以及 TypeScript 等等。Cloudflare 就擁有一個類似的項目,用於在 Workers 上支持 WASM。

緩存

儘可能多地利用緩存處理請求是改善性能水平的關鍵所在。立足 CDN 層級進行緩存,將能夠更快地爲新用戶提供響應。而通過發送 Cache-Control 頭進行緩存,則可加快瀏覽器中重複請求的響應速度。

大多數構建工具(例如 Webpack)允許用戶向文件名當中添加哈希值。由於指向這些文件的任何變更都會產生新的輸出文件名,因此大家可以安心將文件添加至緩存當中。

通過 HTTP/2 進行文件緩存與編碼

GraphQL 緩存

發送 GraphQL 請求的一種常見方法,就是利用 POST HTTP 方法。而我們選擇了立足 Fastly 層級對部分 GraphQL 請求進行緩存:

利用一條 SHA256 URL 參數發送 POST GraphQL 請求

以下是其它一些值得參考的潛在 GraphQL 緩存策略:

編碼

目前,所有主流瀏覽器都支持利用 gzip 加 Content-Encoding 標頭進行數據壓縮。這意味着面向瀏覽器的發送數據量更低,從而帶來更快的內容傳遞速度。此外,如果瀏覽器支持,大家也可以嘗試使用效率更高的 brotli 壓縮算法。

HTTP/2 協議

HTTP/2 是 HTTP 網絡協議的新版本(DevConsole 中簡稱爲 h2)。由於存在着以下幾項與 HTTP/1.x 版本間的顯著差別,切換至 HTTP/2 能夠帶來性能提升:

HTTP/2 Server Push

由於給現有工具及生態系統(例如 rack)引入了一系列顛覆性的變更,很多編程語言與庫並不能完全支持 HTTP/2 的全部功能。但即便如此,我們仍然可以在部分合適的場景中使用 HTTP/2。舉例來說:

HTTP/2 推送字體

對 JavaScript 以及 CSS 的推送功能同樣非常實用。但請注意不要過度推送,您可點擊此處瞭解一些相關問題:https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/

瀏覽器中的 JavaScript

包大小預算

JavaScript 性能優化中的頭號規則就是,不要使用 JavaScript。

—— 我自己

如果您已經擁有現成的 JavaScript 應用程序,那麼設置預算規則能夠提高包大小的可見性,同時確保全部內容都可容納於同一頁面當中。超出預算後,開發人員則需要謹慎考慮並儘量防止規模進一步增長。以下是預算設置方面的相關示例:

您可以使用 bundlesize 工具包或者 Webpack 性能提示與限制進行預算跟蹤:

Webpack 性能提示與限制

消除依賴性

Sidekiq 曾在一篇博文中提到:“代碼越少,運行速度越快。代碼越少,bug 就越少。代碼越少,佔用的內存量就越低。代碼越少,理解起來就越輕鬆。”

遺憾的是,實際 JavaScript 場景中往往存在着不計其數的依賴關係。您可以試試:ls node_modules | wc -l。

在某些情況下,添加依賴性是種必然的選擇。在這種情況下,依賴性的包大小應該被視爲決定您實際工具包選擇的重要依據。我強烈建議大家使用 BundlePhobia:

BundlePhobia 能夠提示將 npm 工具包添加至您數據包中帶來的實際成本

代碼拆分

使用代碼拆分是另一種能夠顯著提高 JavaScript 性能的好辦法。其本質在於分解代碼片段並僅向用戶交付當前所需要的部分。以下是關於代碼拆分的相關示例:

您可以利用 Webpack 動態導入以及 React.lazy 配合 Suspense 實現代碼拆分。

利用動態導入以及 React.lazy 配合 Suspense 實現代碼拆分。

相較於默認導出,我們構建的函數可取代 React.lazy 以支持點名導出。

Async 與 defer 腳本

目前,全部主流瀏覽器皆在 script 標籤上支持 async 與 defer 屬性:

加載 JavaScript 的不同方式

幾種不同的 JavaScript 加載方式:

下面來看 head 標籤下不同腳本間的可視化差異:

幾種不同的腳本抓取與執行方式

圖像優化

雖然與 100 KB 的圖像相比,100 KB 的 JavaScript 代碼明確會帶來更高的性能成本,但我們同樣有必要重視對圖像內容的優化調整。

削減圖像大小的有效手段之一,是在適用的瀏覽器當中採用更加輕量化的 WebP 圖像。對於那些無法支持 WebP 的瀏覽器,大家則可以採取以下幾種策略:

WebP 圖像

僅當圖像位於視圖當中或者附近時才進行內容加載,堪稱多圖像初始頁面加載過程中效果最顯著的提速手段之一。您可以在受支持的瀏覽器當中使用 IntersectionObserver 功能,也可以利用其它一些替代性工具實現相同的結果——例如 react-lazyload。

在滾動過程中進行圖像的延遲加載

其它一些圖像優化策略還包括:

常規圖像與漸進圖像之間的加載效果差異

大家也可以考慮使用通用型 CDN 或者圖像專用 CDN,其通常會直接提供與圖像相關的優化功能。

資源提示

資源提示(Resource hints) 允許我們優化資源交付、降低往返次數,同時獲取資源以實現頁面瀏覽過程中的內容交付提速。

帶有 link 標籤的資源提示

提前進行預連接以避免 DNS、TCP 以及 TLS 往返延遲

當然,prerender 以及 dns-prefetch 等其它一些資源提示同樣非常重要。其中一部分資源提示可在響應標頭中進行指定。需要提醒大家的是,請務必小心使用資源提示。一旦開始濫用,您的頁面中可能包含大量不必要的請求並快速下載過量數據,這種情況顯然不利於使用蜂窩數據的移動用戶。

總結

應用程序的性能改善之路代表着一個永遠盡頭的過程,且通常要求我們在整個堆棧當中持續作出更改。

每次看到下面這段視頻,我總會想起你們努力減少應用包大小的樣子。

——我的同事

馬上把一切不需要的東西從飛機上扔下去!——電影《珍珠港》

以下列出了我們已經使用或者計劃嘗試的其它一些潛在性能改進思路:

另外還有更多令人興奮的想法可供嘗試。希望本文提出的信息及以下案例研究能夠激發出大家改善應用程序性能的更多靈感:

來源:前端之巔

作者:exAspArk

譯者:核子可樂

原文:https://engineering.universe.com/improving-browser-performance-10x-f9551927dcff?gi=ef65642ac481

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NabIRQt_L1XRkC9DN4eKsw