面性能和用戶體驗的各個指標怎么來優化呢?open signal官方提供了2018年2月份統計的全世界4G網絡覆蓋率和通信速率的統計分布圖如下,在目前移動互聯網的浪潮下,我們要利用好用戶終端設備的每個字節的流量。
當然頁面性能和體驗優化并不是一蹴而就的,需要不斷的研究、跟蹤,發現問題,解決問題。但是我們可以在一開始編寫業務代碼的時候就做的更好,做到極致。所以,關于優化實戰我們主要分為兩部分:加載渲染鏈路優化 和 編程代碼優化。
從訪問url到頁面呈現,整個鏈路可以做優化的思路。
幸運的是,W3C推薦的Navigation Timing標準中所定義的核心的頁面性能數據,它包含了從上個頁面銷毀到跳轉到當前頁面加載完成每個階段所消耗的時間。在canIuse上查到的兼容性也很好:
利用這個接口可以很方便的幫助我們排查鏈路問題。在Navigation Timing標準中介紹到這個API主要包含兩個接口:PerformanceTiming和PerformanceNavigation,這兩個接口由瀏覽器進行實現和維護,當瀏覽器創建頁面的時候就會把接口定義的相關數據掛載到window.performance.timing和window.performance.navigation這兩個屬性上。我們可以打開一個網頁看一下:
我們把這兩個圖對比一下,就可以很容易的排查出頁面的加載鏈路問題。
靜態資源鏈路
打開頁面的第一步是請求頁面的html,這里面涉及TTFB這個綜合指標。同時如果有必要我們也可以統計DNS時間和TCP時間。
DNS時間:主要是根據請求域名查詢到對應主機IP的時間。這個和DNS服務器有關系,也可能和本地緩存有關,如果這個很慢,可以找服務商排查下問題。
TCP時間:tcp是承接http協議的下層協議。主要是路由到主機ip,并建立tcp鏈接的時間。這個時間反應了服務器到用戶客戶端之間鏈路是否通暢,網絡是否通暢。
請求完HTML之后,就開始解析html代碼,按照從上至下、自然順序解析,解析內聯CSS代碼或者加載外鏈CSS腳本,解析內聯Javascript腳本,或者加載外鏈Javascript腳本。由于瀏覽器是單線程的,這些CSS和Javascript腳本很可能就會造成頁面卡頓。參考 瀏覽器線程理解與microtask與macrotask。
CDN是內容分發網絡,主要用于緩存靜態資源。CDN服務商一般會在全國各地部署服務,而且帶寬很大,這樣訪問CDN的資源時就可以有較短的路由路徑,而且帶寬也比較大,訪問比較快。
加載完JS和CSS之后,瀏覽器開始解析執行。Chrome的渲染流程是這樣的:(可以參考 高性能CSS動畫)
為了讓瀏覽器更快的解析渲染,我們需要考慮這幾點:
介紹一下code split的方案: react-loadable
// 未處理 import OtherComponent from './OtherComponent'; const MyComponent = () => ( <OtherComponent/> ); // 使用react-loadable按需加載 import Loadable from 'react-loadable'; const LoadableOtherComponent = Loadable({ loader: () => import('./OtherComponent'), loading: () => <div>Loading...</div>, }); const MyComponent = () => ( <LoadableOtherComponent/> );
這個也可以在打包工具統一配置,不用每個模塊都自己寫。
只有瀏覽器盡快渲染出來,用戶才能盡快的可以交互。
上面我們梳理了加載到解析渲染過程應該做的事情,那么如果你這些都做好了,發現網頁表現依然不盡人意,那么你就要考慮做一下數據埋點。其實數據埋點在企業項目中也是必不可少的,和性能體驗優化構成閉環。通過數據來發現頁面性能和體驗的問題,更有針對的進行解決。
事實上數據埋點分為三類:
工程埋點,統計工程上的數據信息,比如頁面秒開率,dns時間等,也就是我們上節課總結的性能和體驗數據指標。
資源緩存
這一節我們單獨介紹緩存,是的,利用好緩存可以解決很多問題,包括頁面加載和渲染的問題都能得到很好的優化。
常見的h5緩存方案有很多種,
通常,與頁面加載性能相關的,有下面幾種緩存,
(1)MemoryCache
MemoryCache,資源存放在內存中,一般資源響應回來就會放進去,頁面關閉就會釋放。內存存取性能可達磁盤緩存性能的100倍,但這還不是MemoryCache的最大優勢,MemoryCache最大的優勢是離排版渲染引擎非常近,可以直接被讀取,甚至無需經過線程轉換。在真實的頁面訪問過程中,獲取資源的時間,磁盤IO僅僅是其中的一部分,更多的時間往往消耗在各種線程拋轉。
(2)ClientCache
ClientCache,客戶端緩存,比如,手淘里的ZCache(離線壓縮包緩存),本質上屬于磁盤緩存。這類Cache的優點是能以相對可控的方式讓資源提前緩存在磁盤,但它也有一系列的成本。比如,它需要一套服務器與客戶端協同的下發更新邏輯,服務器端需要管理下發,客戶端需要提前解壓縮。我們可能覺得提前解壓并不是什么弱點,但如果有一千個離線包,這個問題就比較嚴重了,如果不提前解壓,就無法保證首次訪問性能,如果提前解壓會讓IO非常繁忙,可能會造成客戶端打開時嚴重卡頓。
(3)HttpCache
HttpCache,是歷史比較悠久的緩存,它利用標準的 Cache-Control 與服務器端進行協商,根據標準的規則去緩存或更新資源。它應用非常廣泛,是非常有效果的一種磁盤緩存。它的缺點是完全由瀏覽器按標準規則控制,其它端的控制力度非常弱。比如,某些被HttpCache緩存的靜態資源出問題了,通常只能是改頁面,不再使用出問題的資源,而無法主動清除出問題的資源。參考http請求緩存頭,HTTP協商緩存VS強緩存原理
(4)NetCache
網絡相關的Cache,一般是指DNS解析結果的緩存,或預連接的緩存。DNS預解析和預連接是非常重要的,創建一個Https連接的成本非常大,通常需要600ms以上,也就是說,頁面如果有關鍵資源需要全新建連接,秒開基本是不可能了。
(5)CDN
CDN一般是通過負載均衡設備根據用戶IP地址,以及用戶請求的URL,選擇一臺離用戶比較近,緩存了用戶所需的資源,有較好的服務能力的服務器,讓用戶從該服務器去請求內容。它能讓各個用戶的緩存共享,縮短用戶獲取資源的路徑,來提升整體的性能。
當然,還有其它非常多類型的Cache,比如,
JS相關,V8 Bytecode Cache,字節碼緩存,能極大的減少JS解析耗時,甚至可以提升3-6倍的性能。參考:前端優化系列 – JS解析性能分析
渲染相關,圖片解碼數據緩存,是一塊非常大的內存緩存,約100M,能保證頁面滾動過程可以實時獲取到圖片解碼數據,讓滾動非常流暢。
頁面相關,頁面緩存,Safari的PageCache,Firefox的Back-Forward Cache,UC瀏覽器的WebViewCache,都是一樣性質的緩存,將整個執行過的頁面保存在內存。標準的頁面緩存,進入的條件非常苛刻,大部分情況都無法進入,而且在前進后退的場景才允許使用。
前面介紹了很多理論層面的內容,我們接下來介紹一些實踐優化案例。
(1)預置資源進MemoryCache
在頁面的onPageFinished的回調里面去檢查是否有資源可以預置,如果有,就通過相關接口把資源設置進內核的MemoryCache。我們并不知道用戶即將會訪問什么頁面,如果把大量的資源都預置進內存,而用戶卻沒有使用,那就會造成浪費。另外,資源在內核內存,僅僅是加快了資源的加載速度,頁面的首屏包含非常多非常復雜的流程,某個流程的加速并不一定能帶來整體性能的提升,比如,非關鍵的JS放在內存,可能就會先于一些關鍵JS被提前執行,反而讓首屏更慢。所以,選擇放那些資源進內存也是非常有講究的,能預置的資源一般是 非常關鍵的更新頻率較低的少量公共基礎資源。
對于一般公司來說,沒有能力自己定制webview渲染的內核,可以看下系統默認webview內核有沒有這樣的接口來實現操作MemoryCache預置數據的能力。
(2)預加載資源進HttpCache
預置資源進內存,對加載性能的提升是最明顯的,但成本也是最大的,會占用用戶手機寶貴的內存資源。另外一種預置資源的思路是,提前通過內核去預加載一些資源,資源加載回來之后就直接保存在標準的HttpCache。資源在HttpCache和在客戶端緩存(比如,手淘ZCache)的性能差別不大。但如果資源不能放進ZCache,通過這種方式提前放到HttpCache,也是一種優化思路。
(3)使用WebViewCache極速切換頁面
H5頁面的加載流程是非常重的一套流程,即使同一個頁面多次重復訪問,也需要走比較完整的流程,耗時極長,這與用戶的期望是不符的,通常用戶期望訪問過的頁面就能快速展現出來。在一些特定的場景,H5也是可以做到極速展現的,比如,前進后退。其它的場景,比如頁內幾個TAB切換,是否也可以用上這類緩存呢?也是可以的。原理上也是比較簡單的,在頁面首次訪問時,會將排版渲染好的頁面放進WebViewCache里,WebViewCache是存儲完整頁面的一塊內存。
用戶再次訪問該頁面時,會將WebViewCache內存中的完整頁面讀取出來,直接繪制展現,而無需再進行加載解析排版渲染等流程,從而達到極速打開的效果。
除了內核提供WebViewCache基礎技術之外,前端也需要與內核進行一定的交互,比如,通過JSAPI查詢當前頁面是否在WebViewCache,如果在則返回它在WebViewCache列表的位置,然后前端就可以使用JSAPI去跳轉到相應位置的頁面,內核就把頁面從內存讀取和展現出來。使用此類技術,頁面一般能在500ms左右完全展現出來,具有非常好的用戶體驗。
當然這個也是需要瀏覽器內核提供這種能力,如果公司有自己的內核開發團隊,可以做到定制。
(4)前端使用LocalStorage緩存HTML文檔
當前前端渲染非常流行,頁面大部分的邏輯都會由前端JS去執行,JS執行完才會生成完整的HTML文檔,而JS執行的成本是非常大的,JS執行時間可能占據首屏時間的50%,有些甚至能達到80%。那么,我們有沒有可能將JS執行生成的完整HTML文檔緩存起來呢,下次訪問時直接使用已緩存的頁面,而無需重復執行JS?這也是可以的原理上也不復雜,首次訪問頁面時,JS執行完之后會生成完整的HTML文檔,我們將HTML文檔緩存到LocalStorage里面。
在后續的訪問中,我們優先從LocalStorage里面讀取HTML文檔,解析排版渲染頁面,而無需JS執行去生成頁面,讓頁面展現速度得到極大的提升。
這種方案的關鍵在于前端能夠實現一套DOM-Diff更新的機制,在從LocalStorage讀取HTML頁面的同時,前端還會發起請求去更新HTML文檔,在新的HTML文檔回來之后,會和舊的文檔進行Diff,針對Diff來進行局部更新,這樣能保證頁面得到及時的更新。
(5) service worker
參考使用 Service Workers提升體驗,這里附帶介紹下這個方案,目前service worker 只有在android的webview中可用,ios還不支持。我們通過先注冊一個serviceworker服務,指定哪些資源和數據需要存儲,然后下次請求頁面會自動激活這個service worker,頁面請求時會先從service worker中返回緩存的數據。當然service worker中需要自己處理版本和維護數據更新。
在docker中可以將容器中的目錄掛載出來,在k8s中pod可以部署在不同節點,假如該節點的機器宕機了,k8s可能就會將此Pod轉移到其他機器,就不是原先的機器了。k8s有自己的一套掛載方案,如下圖所示,
原理為將所有節點的掛載的目錄統一抽象管理為叫做 存儲層的概念,使用技術如NFS網絡文件系統,在每一個節點都會有如50G的存儲空間,該空間之間的數據是同步的,這樣就解決了某一節點宕機了,Pod轉移到其他節點了依然可以獲取到掛載的目錄。
如下使用yaml的方式將鏡像nginx中的目錄/usr/share/nginx/html掛載到了nfs服務端的/nfs/data/nginx-pv目錄,只要在掛載目錄/nfs/data/nginx-pv里加文件,也會同步到了pod里/usr/share/nginx/html
kubectl apply -f nfs-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-pv-demo
name: nginx-pv-demo
spec:
replicas: 2
selector:
matchLabels:
app: nginx-pv-demo
template:
metadata:
labels:
app: nginx-pv-demo
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
nfs:
server: 192.168.134.110
path: /nfs/data/nginx-pv
注意掛載的目錄一定要先創建好否則查詢當前應用時出現如下一直在創建中
進入這個pod詳細信息,可以看到報錯的詳細問題 kubectl describe pod nginx-pv-demo-dc6884649-4phvz
這種方式缺點:1、需要提前創建好掛載的機器目錄;2、當Pod刪除,機器掛載的目錄不會同時刪除;3、無法指定目錄的存儲大小
PV持久卷,可以指定大小,當PVC刪除,并釋放對應的PV
PV:持久卷(Persistent Volume),將應用需要持久化的數據保存到指定位置
PVC:持久卷申明(Persistent Volume Claim),申明需要使用的持久卷規格
#nfs主節點
mkdir -p /nfs/k8s/pv/data/01
mkdir -p /nfs/k8s/pv/data/02
mkdir -p /nfs/k8s/pv/data/03
執行命令 kubectl apply -f pvdemo.yaml
以下可以寫在一個文件中 ,用---分割,會自動識別成三個文件,server為主節點的IP地址。storageClassName表示一個標志,在后面創建PVC時需要用到。
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01-10m
spec:
capacity:
storage: 10M
accessModes:
- ReadWriteMany
storageClassName: nfs
nfs:
path: /nfs/k8s/pv/data/01
server: 192.168.134.110
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv02-1gi
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
storageClassName: nfs
nfs:
path: /nfs/k8s/pv/data/02
server: 192.168.134.110
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv03-3gi
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteMany
storageClassName: nfs
nfs:
path: /nfs/k8s/pv/data/03
server: 192.168.134.110
簡寫
pvc.yaml如下:storageClassName: nfs 對應創建的PV
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: nginx-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Mi
storageClassName: nfs
因為申請的storage: 200Mi 指定了大小,所以被分配了擁有1G容量的PV
對應的PV也會被釋放Released
執行 kubectl apply -f pvcdemo.yaml
將創建好的PVC nginx-pvc掛載到當前創建的應用中。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-deploy-pvc
name: nginx-deploy-pvc
spec:
replicas: 2
selector:
matchLabels:
app: nginx-deploy-pvc
template:
metadata:
labels:
app: nginx-deploy-pvc
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
persistentVolumeClaim:
claimName: nginx-pvc
修改掛載的目錄,即可查看文件有沒有同步。
告:
HostPath 卷存在許多安全風險,最佳做法是盡可能避免使用 HostPath。 當必須使用 HostPath 卷時,它的范圍應僅限于所需的文件或目錄,并以只讀方式掛載。
如果通過 AdmissionPolicy 限制 HostPath 對特定目錄的訪問,則必須要求 volumeMounts 使用 readOnly 掛載以使策略生效。
hostPath 卷能將主機節點文件系統上的文件或目錄掛載到你的 Pod 中。 雖然這不是大多數 Pod 需要的,但是它為一些應用程序提供了強大的逃生艙。
例如,hostPath 的一些用法有:
除了必需的 path 屬性之外,你可以選擇性地為 hostPath 卷指定 type。hostPath卷類型支持的type值如下:
取值 | 行為 |
空字符串(默認),用于向后兼容,意味著在安裝hostPath卷之前不會執行任何檢查 | |
DirectoryOrCreate | 如果在給定路徑上什么都不存在,那么將根據需要創建空目錄,權限設置為 0755,具有與 kubelet 相同的組和屬主信息。 |
Directory | 在給定路徑上必須存在的目錄。 |
FileOrCreate | 如果在給定路徑上什么都不存在,那么將在那里根據需要創建空文件,權限設置為 0644,具有與 kubelet 相同的組和所有權。 |
File | 在給定路徑上必須存在的文件。 |
Socket | 在給定路徑上必須存在的 UNIX 套接字。 |
CharDevice | 在給定路徑上必須存在的字符設備。 |
BlockDevice | 在給定路徑上必須存在的塊設備。 |
當使用這種類型的卷時要小心,因為:
hostPath卷類型測試
測試選擇在第二個節點服務器上使用hostPath,將Pod調度限制在該節點,如果不加以限制,Pod可能會被調度到其他節點服務器上,Pod無法啟動。
// hostPath 類型卷配置文件
[root@c7-1 hostpath-manual-storageclass]# cat pv-volume-hostpath.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-hostpath10g
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data"
// pvc 申領配置文件
[root@c7-1 hostpath-manual-storageclass]# cat pvc-hostpath-manual.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-hostpath10g
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
// Pod 配置文件,引用hostPath類型的卷
[root@c7-1 hostpath-manual-storageclass]# cat pod-hostpath.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-pv-hostpath
spec:
nodeName: c7-2.runoqd.com // 指定該Pod被調度到的節點
volumes:
- name: pv-storage10g
persistentVolumeClaim:
claimName: pvc-hostpath10g
containers:
- name: pod-pv-hostpath
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage10g
創建相關資源并驗證
*請認真填寫需求信息,我們會在24小時內與您取得聯系。