我所知道的 Web 效能優化策略

rccoder發表於2019-03-24

前言

本文核心分為兩部分,第一部分講述普通瀏覽器中能幹的事情,第二部分則講述在自建容器的背景下更能幹的事情。

文章內容會比較粗略,如果你對具體實現感興趣,歡迎在 留言。

一、止步於瀏覽器

1.1 DNS Prefetch

通常情況下,一個 html bundle 裡面一般會有 script 等標籤去載入其他的資源。瀏覽器在載入完 html 之後,就會去載入 script 等標籤裡面的內容,大多情況下,這種標籤裡 uri 的 host 和當前頁面的往往是不相同的,那就會涉及到 DNS 解析的問題,會有一定程度的損耗。

在 HTML 裡面加入 DNS Prefetch 則會讓瀏覽器提前進行 DNS 的解析並且快取到系統中,這樣就可以提升網頁的載入效率了。

<link rel="dns-prefetch" href="//img.alicdn.com">
複製程式碼

1.2 域名收斂

在像 v2ex 這樣的社群論壇中,往往會有很多使用者外鏈圖片,不同的圖片有不同的域名。這個時候 DNS Prefetch 會顯得很無力,如果說在圖片上傳之後做一定的 轉化 或者 同步,把域名收斂到同一個域名中,這樣就能彌補相關的缺憾了。

aaa.com/jjj.png ->  mycdn.com/aaa/jjj.png
bbb.com/jjj.png ->  mycdn.com/bbb/jjj.png
複製程式碼

1.3 載入合適的圖片

同一個圖片在 在不同的裝置、不同的網路條件、不同的渲染面積下,統一載入一樣的尺寸難免是 “奢侈” 的,利用 CDN 裁剪 + 前端嗅探 去載入不同體積、不同壓縮比率、不同格式的圖片,能在節省不少流量的同時提升很大的效能。

目前主流 CDN / OSS 都是支援通過 URL 字尾進行圖片裁剪的,比如:阿里雲

http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/crop,x_100,y_50
複製程式碼

在前端嗅探上,一般我們可以嗅探 是否支援 webp、客戶端解析度、當前網路狀況,去全域性設定不同的圖片載入 URL。

base64、

1.4 不要展示 “絕對Loading / 佔位”

當前時代,大部分網頁的資料都是動態下發的,甚至千人前面,為了減少使用者等待的焦灼感,往往會設定一個 Loading 動畫或者 骨骼圖佔位。但當請求響應足夠快的時候,會發現這種 Loading 或 佔位 卻會給人相反的感覺 —— 瞬間的閃動。

針對這種情況,在使用 Loading 或骨骼圖佔位的,可以做一定的優化,比如請求發起後 200ms 以上還未返回資料才展示佔點陣圖。在 React Suspense 中,為了這種效果官方甚至加了一個 API。

1.5 資源 combo

在 HTTP 請求中,請求建立往往因為 TCP 三次握手等會有一個非常大的開銷。涉想在 HTTP 1.0 時代,如果頁面上有 50 個 Script 標籤,會有 50 次的請求建立,在資源不是那麼大的情況下,請求建立的耗時往往會遠大於資源真正的下載時間。

在服務端做資源 Combo,然後再往 CDN 回源,就能很大程式上減少這種開銷。

xx.com/a.js
xx.com/b.js
xx.com/c.js
xx.com/d.js

xx.com/combo?a.js,b.js,c.js,c.js
複製程式碼

在 HTTP 2.0 時代資源要不要 Combo + 多少個資源(多大的體積)Combo 到一起 是個有意思的話題,這裡不做討論。

1.6 線上 Shim

前端因為瀏覽器問題一直存在店鋪這種東西,有很多的特性往往在新的瀏覽器版本里面已經支援,但奈何老的不支援需要做墊片。如果對程式碼統一加墊片則又會讓新瀏覽器很尷尬(我要這新特性有何用?),使用線上的 Shim 應該是一個不錯的 Shim。

比如著名的 polyfill.io/v3/url-buil… 就是一個這樣的服務。在支援 Object.assign 中訪問 polyfill.io/v3/polyfill… , 會得到:

/* Disable minification (remove `.min` from URL path) for more info */
複製程式碼

而在不支援的瀏覽器中訪問,會得到:

(function(undefined) {if (!(// In IE8, defineProperty could only act on DOM elements, so full support
// for the feature requires the ability to set a property on an arbitrary object
'defineProperty' in Object && (function() {
	try {
		var a = {};
		Object.defineProperty(a, 'test', {value:42});
		return true;
	} catch(e) {
		return false
	}
}()))) {!function(e){
...
複製程式碼

在國內也有這樣的服務,比如:polyfill.alicdn.com/polyfill.mi… ,如果你覺得不夠信賴,可以自建然後部署在 CDN 上。

1.7 分離靜態資源

在絕大多數情況下,訪問靜態資源的時候並不需要知道 Cookie 資訊,為靜態資源使用一個新的域名能有效避免用不著的 Cookie 上傳,能減少一部分無用流量的傳輸。

1.8 使用 requestAnimationFrame 實現 60 FPS 動畫

在繪製動畫的時候,優先使用 requestAnimationFrame,會充分利用分利用顯示器的重新整理機制,實現 60 FPS 的感覺。

1.9 節流 & 防抖

在 Web 中,像 Scroll 這種事件,在介面操作中觸發的頻率是非常之高的。涉想這樣的一種場景:使用者往下滑動網頁,當滑動距離超過 1000px 的時候,右下角展現一個 回到頂部 的按鈕。想當然的操作就是監聽 Scroll 事件,當值大於 1000px 的時候展示 按鈕,但因為 Scroll 的高頻率觸發,尤其在移動端這樣做就能感覺到比較明顯的效能問題了,如果我們對其加個操作 —— 1s 內檢測函式只觸發 1 次 或者在使用者停下來的時候再去檢測位置。這樣頁面整體就會流暢很多了,相對應的兩個操作就是 節流 和 防抖。

在 Web 開發過程中,對於這種高頻次觸發的事情,合理的程式節流和防抖能在很大程度上增加頁面流產度。

1.10 使用新版本 JavaScript

一般情況下,V8 等 JavaScript 的 Runtime 都會對新特性有優化,在新瀏覽器上使用 Babel 轉化後的程式碼難免會有一定的浪費與可惜。在新瀏覽器上使用新預發,老瀏覽器上使用老語法,才是比較好姿勢。

實現上一種思路就是線上 Shim;第二種思路是正對先載入 seed 再載入 bundle 程式碼,可以在載入 bundle 之前做一個知否支援新版本 ES 的判斷(比如是否支援 async 函式),然後再載入相應的 Bundle。

1.11 效能測量

window.performace 能展示絕大多數檢測 Web 效能的指標,在業務程式碼中埋點收集 window.performance 的值,可以為網頁效能短板做很好的測量與統計。

1.12 善用 LocalStorage

在一些場景下,每次使用者進入時資料的變化不會太大,比如不怎麼更新的個人部落格頁面。這個時候就可以使用 LocalStorage 去做 HTML 的快取,頁面進入的時候直接從 Storage 中獲取快取,然後 append 到頁面上,等介面資料回來之後,再 Diff 做更新。

在新版本瀏覽器中,可以用 indexedDB 等代替 LocalStorage。

1.13 元件級快取

在 SPA 網站中,載入 bundle 大致上可以分為兩份:

  1. 所有的元件程式碼和業務程式碼打到一起,和業務程式碼一起輸出
  2. 元件程式碼在元件內部各自打包,業務程式碼打包的時候 external 掉元件程式碼,最後 combo 到一起輸出。

針對第二種情況,可以利用 LocalStorage 等單獨快取元件程式碼(帶上版本號),在端側實現一個 Combo 的機制(有 Cache 取 Cache,沒 Cache Fetch),這樣一來,就能讓一個網站的多個頁面享受同份快取,讓之第一次也能非常快速的訪問。

1.14 GZIP & BBR

GZIP 壓縮使用 Deflate 能有效壓縮文字資源的大小,在現代瀏覽器中,對 GZIP 的支援也是非常良好。值的注意的是,GZIP 的壓縮並不是壓的越小越好,太小會產生壓縮效能的問題。

傳統 TCP 使用的是基於丟包的擁塞控制演算法,但並不是所有的丟包都是網路堵塞所導致的,為此 Google 開發了 BBR 擁塞控制演算法,能有效提升伺服器的吞吐量,如果伺服器支援的話,可以開啟 BBR 來加快網路傳輸。

1.15 Service Worker

Service Worker 本質上充當Web應用程式與瀏覽器之間的代理伺服器,利用 Service Worker 可以極致的控制每個請求,進而可以對 Web APP 在瀏覽器上做離線處理。

傳說中的 PWA 就是對這個東西的一個極致應用。

1.16 WebWorker

WebWorker 為 JavaScript 在瀏覽器中多執行緒呼叫提供了能力,可以讓主執行緒建立 Worker 執行緒,針對一些密集計算或者需要時間比較高的場景,是非常有效的。比如:網頁版郵箱附件上傳等。

二、外探於自建容器

2.1 WKWebview

從 iOS8 開始,iOS 提供了 WKWebview 來代替 UIWebview。相比於 UIWebview,在效能和記憶體控制上都有非常大的提升,當然問題也是有的,比如 Cookie 同步問題等,但坑總能趟過去。

回到優點上,WKWebview 給前端最直接的體驗莫過於 “Scroll 終於不再需要滾動停止下來才能觸發了”,進而 LoadMore 等會更加的流暢。

2.2 Webviw 核心內建

Android 的碎片化一直是一個很嚴重的問題,即使是在今天。通過內建高效能的 Webview(比如 U4、X5)等,會為整個 APP Web 頁面提速不少,在相容性方面,也會好很多。

2.3 資源 Cache

要實現頁面秒開效果,Cache 肯定是第一優先順序,通過下發離線包,讓頁面上的資源(HTML,CSS)離線,就可以很大程度上提升頁面的效能。

在離線體系建設上,主要有兩點需要考慮:1. 離線如果能快速下發,快速覆蓋新版本 2. 前端如何才能無感知接入離線體系。

針對第一個問題,往往採取推+拉結合的方式;針對第二個問題,在實現的時候採用攔截網路的策略,就可以避免前端對離線的感知了

2.4 代理請求

如前文所述,建立一個 HTTP 請求是非常耗時的,從客戶端的角度來看,是可以去優化這種請求的,比如被廣泛使用的 Spidy。

在 WebAPP 中,發出一個資料請求,讓走容器的通過而非 WebView 的通道,不僅有機會能讓速度變快,同時還可以進行相應的加密,讓抓包者懵逼。

2.5 資料預載入

試想一個網頁的載入過程,載入 HTML -> 載入 JS -> 執行 JS -> 請求資料 -> 再次渲染。請求資料的流程是比較靠後的,在 WebAPP 中,如果能讓這個請求或者請求到的資料提前,則會進一步提升網頁速度。

一個比較常用的方式是往客戶端下發一份配置,標識 頁面地址、請求入參、快取時間等資訊,讓客戶端在訪問這個 URL 的時候去載入資料,等前端程式碼真正請求的時候,直接拿客戶端提前請求到的資料;或者說在前一個頁面呼叫介面通知客戶端去請求相關的介面。

目前這種做法應用比較廣泛,比如:微信公眾號,UC Feed 流等。

2.6 閹割版 WebView

WebView 因為各種歷史原因,“慢” 一直徘徊在他的左右。去定製 WebView,甚至去實現一層上層 DSL,只儲存比較優秀的部分,也能在限制部分場景的同時去提升頁面效能。

最典型的設計就是微信小程式了,閹割版的 WebView + Cache 機制,很難讓人會覺得這是一個 WebAPP

2.7 Weex/React Native

Weex/RN 相比於閹割版 WebView,是一個更加激進的優化訪問。核心原理是自己造一個高效能的 Runtime,和客戶端高度配置,由 JS Bundle 去 call native,用 Native 的方式去渲染頁面。

同時對於 Web 上效能上有問題的部分,可以用傳入表示式的方式讓 Native 去執行。比如註明的 BindingX,就是傳入一個表示式,然後去客戶端去執行這個表示式,進而避免了頻繁 call native 的問題,這樣就可以保證動畫的流暢執行了。

2.8 Flutter

...

原文地址:github.com/rccoder/blo…

相關文章