適合所有網站的前端優化技巧,值得你收藏!
導讀:本文雖然明指了某個主頁,但是其實是適合所有網站系統前端的優化技巧的。
最近,我們將 Universe.com 主頁的性能提升了十倍以上。在本文中,我們將解析實現這一重大改進的具體技術手段。
但在開始之前,讓我們先對網絡性能的重要意義進行一番論證(博文末尾提供相關案例研究鏈接):
-
用戶體驗: 糟糕的性能可能導致響應失敗,從 UI 與 UX 的角度來看,這可能會引發用戶的沮喪情緒。
-
客戶轉化與收入: 網站速度緩慢通常會導致客戶流失,並對轉化率與收入產生負面影響。
-
SEO: 從 2019 年 7 月 1 日開始,谷歌公司開始在全部新網站上默認啓用移動優先索引。如果網站在移動設備上運行緩慢,且沒有針對移動設備進行內容格式調整,那麼網站的搜索排名將會降低。
在本篇文章中,我們將簡要介紹以下幾大有助於我們提高頁面性能的主要領域:
-
性能測量: 實驗室與現場工具測量。
-
渲染: 客戶端與服務器端渲染、預渲染以及混合渲染方法。
-
網絡: CDN、緩存、GraphQL 緩存、編碼、HTTP/2 以及 Server Push。
-
瀏覽器中的 JavaScript: 數據包大小預算、代碼拆分、async 與 defer 腳本、圖像優化(WebP、延遲加載、漸進式設計)以及資源提示(preload、prefetch 與 preconnect)。
這裏再介紹一點我們的情況:我們的主頁由 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 — 允許用戶立足不同位置上的實際設備對不同瀏覽器進行性能測試。
-
Test My Site — 使用 Chrome 用戶體驗報告 (CrUX) 功能,並以 Chrome 使用情況統計爲基礎;這款工具公開可用且每月更新一次。
-
PageSpeed Insights — 將實驗室(Lighthouse)與現場(CrUX)數據加以結合。
WebPageTest 報告
渲染
內容的渲染可通過多種方法實現,其中每一種都擁有獨特的優勢與缺點:
-
服務器端渲染 (SSR) 是指在服務器端爲瀏覽器提供最終 HTML 文檔的過程。優勢:搜索引擎可以直接抓取網站而無需執行 JavaScript(SEO)、快速初始頁面加載、代碼僅存在於服務器端。短板:非富網站交互、整頁重新加載、瀏覽器功能受限。
-
客戶端渲染是指利用 JavaScript 在瀏覽器當中進行內容渲染的過程。優勢:富網站交互、在初始加載後可快速呈現路由變更內容、支持現代瀏覽器功能(例如配合 Service Workers 實現離線支持)。短板:SEO 友好性差、初始頁面加載緩慢、通常需要在服務器端實現單頁面應用程序(SPA)與 API。
-
預渲染類似於服務器端渲染方法,但渲染會提前發生在構建時而非運行時。優勢:built 靜態支持文件通常比服務器運行方法更簡單、SEO 友好性高、快速初始頁面加載。短板:需要在執行任何代碼變更時提前進行完整頁面重新加載、非富網站交互、瀏覽器功能訪問限制。
客戶端渲染
以前,我們將自己的主頁與 Ember.js 框架一同實現爲採用客戶端渲染方法的單頁面應用。但這種做法的一大問題在於,我們的 Ember.js 應用程序包過大。這意味着在瀏覽器下載 JavaScript 文件並對其進行解析、編譯與執行的過程中,用戶只能對着空白屏幕發呆:
最要命的空白屏幕
因此,我們決定利用 React 重構應用當中的某些部分。
-
我們的開發人員已經非常熟悉 React 應用程序的構建方法(例如嵌入式功能部件)。
-
我們已經擁有多個 React 組件庫可在多個項目間隨意共享。
-
新的頁面中將可包含一些交互式 UI 元素。
-
龐大的 React 生態系統能夠提供多種工具方案。
-
利用瀏覽器中的 JavaScript,我們可以通過多項強大功能構建起漸進式 Web 應用。
預渲染與服務器端渲染
客戶端渲染應用程序的具體構建——例如採用 React Router DOM,仍然會帶來與 Ember.js 相同的問題。JavaScript 需要佔用大量資源,而且訪問者需要經歷一段首屏內容填充週期才能看到實際內容。
因此在決定使用 React 之後,我們開始嘗試其它潛在的渲染選項,以確保瀏覽器能夠更快地完成內容渲染。
使用 React 時的常規渲染選項
-
Gatsby.js 允許我們利用 React 與 GraphQL 構建預渲染頁面。Gatsby.js 是一款強大的工具,能夠直接提供多種性能優化方案。然而,預渲染方法並不適合我們的需求,因爲我們的網站中可能存在無數包含用戶生成內容的頁面。
-
Next.js 是一套高人氣 Node.js 框架,允許用戶通過 React 實現服務器端渲染。然而,Next.js 設定了太多條條框框,要求用戶使用它提供的路由機制以及 CSS 解決方案等等。另外,我們的現有組件庫是專爲瀏覽器構建的,與 Node.js 並不兼容。
因此,我們打算嘗試一下混合方法,即發揮每一種渲染選項中的獨特優勢。
運行時預渲染
Puppeteer 是一套 Node.js 庫,允許用戶使用 headless Chrome。我們希望嘗試利用 Puppeteer 在運行時當中實現預渲染。這代表着一種有趣的混合方法:利用 Puppeteer 進行服務器端渲染,同時利用 hydration 進行客戶端渲染。感興趣的朋友可以點擊此處查看谷歌提供的關於如何利用 headless 瀏覽器進行服務器端渲染的相關提示。
利用 Puppeteer 對 React 應用程序進行運行時預渲染
這種方法具備以下優勢:
-
允許 SSR,因此有利於 SEO 優化。抓取程序不需要執行 JavaScript 即可看到網頁內容。
-
允許一次性構建起簡單的瀏覽器 React 應用程序,而後將其同時用於服務器端與瀏覽器內。這將同時提高瀏覽器應用與 SSR 的速度表現,一舉兩得。
-
利用 Puppeteer 在服務器端渲染頁面,在速度上一般快於在最終用戶的移動設備上進行渲染(前者網絡連接更強、硬件配置也更高)。
-
Hydration 允許我們構建起富 SPA,並可訪問 JavaScript 的瀏覽器功能。
-
我們不再需要預先了解所有可能被調用的頁面,也不需要預先進行渲染。
但在採用這種方法的過程中,我們也遇到了一些挑戰:
- 吞吐量是最主要的問題。每項請求都會在單獨的 headless 瀏覽器進程當中佔用大量資源。雖然我們可以使用單一 headless 瀏覽器進程並在其中的各個選項卡內運行多項請求,但使用多個選項卡仍會降低整個進程的性能水平。
利用 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 則用於實現服務器端渲染
- Puppeteer 在構建時中按照我們預期的方式對 React 頁面進行預渲染,並將結果保存爲 HTML 文件(來自 PRPL 模式的 app shell)。
我們可以繼續構建一款簡單的瀏覽器 React 應用程序,並在無需等待最終用戶設備 JavaScript 處理過程的同時獲得快速初始頁面加載效果。
-
我們的 Phoenix 應用負責實現頁面預渲染,並以動態方式將實際內容注入至 HTML。這就使得內容的 SEO 友好性大幅提升,讓按需處理大量多種頁面成爲可能,並顯著降低了擴展難度。
-
客戶端接收並立即開始顯示 HTML,而後由 Hydration 將 React DOM 狀態持續作爲常規 SPA。如此一來,我們就構建起了高度交互的應用程序,並可訪問各項 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 請求進行緩存:
-
我們的 React 應用會標註出那些可進行緩存的 GraphQL 查詢。
-
在發送 HTTP 請求之前,我們以請求本體爲基礎構建一條附加 URL 參數,其中包含 GraphQL 查詢與變量(我們配合 Apollo Client 使用自定義 fetch)。
-
在默認情況下 ,Varnish(與 Fastly)會使用完整的 URL 作爲緩存密鑰的一部分。
-
這意味着我們可以通過請求本體當中的 GraphQL 查詢不斷髮送 POST 請求,並在無需接觸服務器的前提下立足邊緣位置完成緩存。
利用一條 SHA256 URL 參數發送 POST GraphQL 請求
以下是其它一些值得參考的潛在 GraphQL 緩存策略:
-
服務器端緩存:立足解析器層級或者通過模式標註對全部 GraphQL 請求進行緩存。
-
利用持久化 GraphQL 查詢併發送 GET /graphql/:queryId 以使用 HTTP 緩存機制。
-
利用自動化工具(例如 Apollo Server 2.0)或者 GraphQL 專用型 CDN(例如 FastQL)實現不同 CDN 的整合。
編碼
目前,所有主流瀏覽器都支持利用 gzip 加 Content-Encoding 標頭進行數據壓縮。這意味着面向瀏覽器的發送數據量更低,從而帶來更快的內容傳遞速度。此外,如果瀏覽器支持,大家也可以嘗試使用效率更高的 brotli 壓縮算法。
HTTP/2 協議
HTTP/2 是 HTTP 網絡協議的新版本(DevConsole 中簡稱爲 h2)。由於存在着以下幾項與 HTTP/1.x 版本間的顯著差別,切換至 HTTP/2 能夠帶來性能提升:
-
HTTP/2 爲二進制,而非文本式。因此其解析效率更高,也更加緊湊。
-
HTTP/2 具有多路複用屬性,這意味着 HTTP/2 可以通過單一 TCP 連接發送多項請求。如此一來,我們就不必擔心每主機瀏覽器連接限制以及域名分片等問題。
-
其利用標頭壓縮機制減少請求 / 響應的實際體積。
-
允許服務器主動推送響應。這項功能擁有諸多有趣的實際應用方式。
HTTP/2 Server Push
由於給現有工具及生態系統(例如 rack)引入了一系列顛覆性的變更,很多編程語言與庫並不能完全支持 HTTP/2 的全部功能。但即便如此,我們仍然可以在部分合適的場景中使用 HTTP/2。舉例來說:
-
利用 HTTP/2 在常規 HTTP/1.x 服務器之前設置一套 h2o 或者 nginx 代理服務器。Puma 與 Ruby on Rails 能夠發送 Early Hints,從而在一定的限制條件下啓用 HTTP/2 Server Push。
-
利用支持 HTTP/2 的 CDN 交付靜態資產。例如,我們可以使用這種方法將字體以及一部分 JavaScript 文件推送至客戶端。
HTTP/2 推送字體
對 JavaScript 以及 CSS 的推送功能同樣非常實用。但請注意不要過度推送,您可點擊此處瞭解一些相關問題:https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/
瀏覽器中的 JavaScript
包大小預算
JavaScript 性能優化中的頭號規則就是,不要使用 JavaScript。
—— 我自己
如果您已經擁有現成的 JavaScript 應用程序,那麼設置預算規則能夠提高包大小的可見性,同時確保全部內容都可容納於同一頁面當中。超出預算後,開發人員則需要謹慎考慮並儘量防止規模進一步增長。以下是預算設置方面的相關示例:
-
根據您的實際需求或推薦值設定數值。例如,不得大於 170 KB 否則壓縮 JavaScript。
-
利用現有包大小作爲基準,或者嘗試對其進行削減——例如下調 10%。
-
嘗試讓網站擁有高於競爭對手的速度,並以此爲依據設定預算。
您可以使用 bundlesize 工具包或者 Webpack 性能提示與限制進行預算跟蹤:
Webpack 性能提示與限制
消除依賴性
Sidekiq 曾在一篇博文中提到:“代碼越少,運行速度越快。代碼越少,bug 就越少。代碼越少,佔用的內存量就越低。代碼越少,理解起來就越輕鬆。”
遺憾的是,實際 JavaScript 場景中往往存在着不計其數的依賴關係。您可以試試:ls node_modules | wc -l。
在某些情況下,添加依賴性是種必然的選擇。在這種情況下,依賴性的包大小應該被視爲決定您實際工具包選擇的重要依據。我強烈建議大家使用 BundlePhobia:
BundlePhobia 能夠提示將 npm 工具包添加至您數據包中帶來的實際成本
代碼拆分
使用代碼拆分是另一種能夠顯著提高 JavaScript 性能的好辦法。其本質在於分解代碼片段並僅向用戶交付當前所需要的部分。以下是關於代碼拆分的相關示例:
-
在不同的 JavaScript 代碼塊間分別加載路由機制。
-
拆分那些在頁面中無法立即顯示的部分,例如彈出框以及頁面下方的頁腳。
-
Polyfills 與 ponyfills 可支持全部主流瀏覽器當中的各最新瀏覽器功能。
-
利用 Webpack 的 SplitChunksPlugin 防止代碼重複。
-
按需定位文件,以避免一次性發送所有受支持的語言。
您可以利用 Webpack 動態導入以及 React.lazy 配合 Suspense 實現代碼拆分。
利用動態導入以及 React.lazy 配合 Suspense 實現代碼拆分。
相較於默認導出,我們構建的函數可取代 React.lazy 以支持點名導出。
Async 與 defer 腳本
目前,全部主流瀏覽器皆在 script 標籤上支持 async 與 defer 屬性:
加載 JavaScript 的不同方式
幾種不同的 JavaScript 加載方式:
-
內聯腳本適用於加載小體積、高關鍵度 JavaScript 代碼。
-
當您的用戶或者任何其它腳本(例如分析腳本)不再需要某些特定腳本時,大家可以將 async 與這些腳本配合使用以避免 HTML 解析阻塞。
-
從性能角度來看,將 defer 與腳本配合使用能夠有效提升非關鍵 JavaScript 代碼的抓取與執行效率,且避免發生 HTML 解析阻塞。此外,這種做法還能夠在調用腳本時保證執行順序,從而確保不同腳本間存在依賴性時實時與預期相符的執行效果。
下面來看 head 標籤下不同腳本間的可視化差異:
幾種不同的腳本抓取與執行方式
圖像優化
雖然與 100 KB 的圖像相比,100 KB 的 JavaScript 代碼明確會帶來更高的性能成本,但我們同樣有必要重視對圖像內容的優化調整。
削減圖像大小的有效手段之一,是在適用的瀏覽器當中採用更加輕量化的 WebP 圖像。對於那些無法支持 WebP 的瀏覽器,大家則可以採取以下幾種策略:
-
回退至常規的 JPEG 或者 PNG 格式(某些 CDN 會根據瀏覽器的 Accept 請求標頭自動執行)。
-
在檢測瀏覽器的支持情況後,加載並使用 WebP polyfill。
-
利用 Service Workers 監聽 fetch 請求,並在支持時利用 WebP 變更實際 URL。
WebP 圖像
僅當圖像位於視圖當中或者附近時才進行內容加載,堪稱多圖像初始頁面加載過程中效果最顯著的提速手段之一。您可以在受支持的瀏覽器當中使用 IntersectionObserver 功能,也可以利用其它一些替代性工具實現相同的結果——例如 react-lazyload。
在滾動過程中進行圖像的延遲加載
其它一些圖像優化策略還包括:
-
降低圖像質量以減小體積。
-
調整大小並加載最小圖像。
-
利用 Srcset 圖像屬性自動在高分辨率顯示器上加載高質量圖像。
-
利用漸進式圖像快速顯示圖像的模糊版本。
常規圖像與漸進圖像之間的加載效果差異
大家也可以考慮使用通用型 CDN 或者圖像專用 CDN,其通常會直接提供與圖像相關的優化功能。
資源提示
資源提示(Resource hints) 允許我們優化資源交付、降低往返次數,同時獲取資源以實現頁面瀏覽過程中的內容交付提速。
帶有 link 標籤的資源提示
-
Preload 會在當前頁面實際使用之前,通過後臺預先下載高優先級資源。
-
Prefetch 的功能與 preload 類似,用於抓取資源並進行緩存,但僅供用戶後續導航使用(低優先級)。
-
Preconnect 允許 HTTP 請求被實際發送至服務器之前即設置預連接。
提前進行預連接以避免 DNS、TCP 以及 TLS 往返延遲
當然,prerender 以及 dns-prefetch 等其它一些資源提示同樣非常重要。其中一部分資源提示可在響應標頭中進行指定。需要提醒大家的是,請務必小心使用資源提示。一旦開始濫用,您的頁面中可能包含大量不必要的請求並快速下載過量數據,這種情況顯然不利於使用蜂窩數據的移動用戶。
總結
應用程序的性能改善之路代表着一個永遠盡頭的過程,且通常要求我們在整個堆棧當中持續作出更改。
每次看到下面這段視頻,我總會想起你們努力減少應用包大小的樣子。
——我的同事
馬上把一切不需要的東西從飛機上扔下去!——電影《珍珠港》
以下列出了我們已經使用或者計劃嘗試的其它一些潛在性能改進思路:
-
使用 Service Workers 進行緩存、離線支持以及主線程分攤。
-
通過關鍵 CSS 內聯或者函數式 CSS 實現數據包的長效 “瘦身”。
-
使用 WOFF2 字體替代 WOFF 字體(僅舉一例,字體變更最高可帶來 50% 壓縮效果)。
-
確保 browserslist 的定期更新。
-
利用 webpack-bundle-analyzer 直觀分析構建塊。
-
優選較小的工具包(例如 date-fns)及插件(例如 lodash-webpack-plugin),從而縮小頁面體積。
-
嘗試使用 preact、lit-html 或者 svelte。
-
在 CI 中運行 Lighthouse。
-
漸進式 hydration 與 React 流式設計。
另外還有更多令人興奮的想法可供嘗試。希望本文提出的信息及以下案例研究能夠激發出大家改善應用程序性能的更多靈感:
-
根據亞馬遜方面的計算,單一頁面 1 秒的響應延時每年可能造成 16 億美元損失。鏈接地址:https://www.fastcompany.com/1825005/how-one-second-could-cost-amazon-16-billion-sales
-
沃爾瑪每縮短 1 秒加載時長,即可提升 2% 的客戶轉換率。每 100 毫秒的提升則可帶來 1% 的收入增長。鏈接地址:https://wpostats.com/2015/11/04/walmart-revenue.html
-
谷歌公司計算出,如果搜索結果顯示速度減緩 0.4 秒,則每天搜索量將減少 800 萬次。鏈接地址:https://www.fastcompany.com/1825005/how-one-second-could-cost-amazon-16-billion-sales
-
品趣志的頁面重構將等待時長縮短了 40%,SEO 流量增加了 15%,註冊轉換率亦提升 15%。鏈接地址:https://medium.com/@Pinterest_Engineering/driving-user-growth-with-performance-improvements-cfc50dafadd7
-
BBC 通過觀察發現,網站加載時長每增加 1 秒鐘,就會失去 10% 的用戶。鏈接地址:https://www.creativebloq.com/features/how-the-bbc-builds-websites-that-scale
-
FT.com 通過測試證明,更快的響應速度令用戶的參與度提高了 30%——這意味着更多的訪問次數與更大的內容消費總量。鏈接地址:https://www.wsj.com/articles/financial-times-hopes-speedy-new-website-will-boost-subscribers-1475553602
-
Instagram 通過降低顯示評論內容所需的 JSON 響應包的大小,成功將展示次數與用戶個人資料滾動操作量增加了 33%。鏈接地址:https://instagram-engineering.com/performance-usage-at-instagram-d2ba0347e442
來源:前端之巔
作者:exAspArk
譯者:核子可樂
原文:https://engineering.universe.com/improving-browser-performance-10x-f9551927dcff?gi=ef65642ac481
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/NabIRQt_L1XRkC9DN4eKsw