關於原文
原文是在Medium上面看到的,Chrome工程師Addy Osmani釋出的一篇文章,這位的Medium上面的自我介紹裡面有一句Passionate about making the web fast,和這篇文章的主體可以說非常契合了。
最近在做一個服務端渲染的專案,到底頁面效能的提升能夠帶來多少的意義,或者到底有多少合理的方法來讓精雕細琢移動端的使用者體驗。
這篇文章主要是部分內容的翻譯,並且結合自己的一些想法,以備在專案架構的時候考慮到這些相關的東西。
原文內容會按照引用樣式排版
原文-The Cost Of JavaScript 2018
這篇文章很乾,幹到一張圖都不存在,所以想讀下去的請做好準備吧~,更推薦直接去讀原文哦!!
前言
首先,JavaScript仍舊是我們傳送到使用者移動裝置上的最昂貴的資源,因為它可以在很大程度上延遲使用者的互動。
JavaScript阻塞頁面上的互動動作,所有同步執行的JavaScript程式碼不會中斷自己的執行來給突然觸發的互動事件讓路,並且如果在互動的時候需要頁面樣式的變化,也是會被阻塞的。React 16中新加入的Fiber功能,就是為了將同步的re-render操作分片,來讓頁面的互動可以間歇進行而不是完全阻塞。關於React Fiber的原理,可以看這一篇:React 16.0 Fiber原始碼解讀。
React在Release Note中也寫到了,React 16還有一大進步就是相比起15.6版本來說,其react
庫壓縮到了5.3kb,gzip壓縮後可以達到2.2kb,而react-dom
庫也從141kb壓縮到了103.7kb,庫大小的減少也能夠很好地提升React在客戶端的執行速度,在頁面首次渲染的時候載入更少的資源。
- 為了保證速度,僅僅載入當前頁面必須的JavaScript;
- 提前做好效能預算,並且合理利用;
- 做好JavaScript打包以及程式碼審計工作;
- 每一個互動都是從一個新的“可互動時間”開始的,考慮如何在這種情況下進行優化;
- 如果一段客戶端JavaScript並不能夠提升使用者體驗,那麼就要問問你自己這段程式碼是否是必須的。
原文的文章很長,所以放了一個tl;dr:在文章最前面,文章的五個重點都列出來了,如何壓榨每一個Byte的JavaScript的效能,需要從多個方面考慮可精簡的JavaScript。
web由於使用者“體驗”而膨脹
也許你根本就不知道自己頁面中的JavaScript到底佔據了多少物理資源來執行,尤其是在移動裝置上。目前的現代網頁平均會使用250KB的壓縮JavaScript,如果沒有壓縮的話,大概是1MB左右的指令碼,這些指令碼都需要瀏覽器來執行,無論是在移動裝置還是PC上面。
在公司網路的環境下,使用lighthouse來對網易雲音樂的首頁進行檢測,從開始HTTP請求一直到頁面開始可以互動的時間大概在3s左右,根據5s原則來說,已經是一個很好的體驗了,很多元素的延遲載入起到了很好的作用。如果你想看到自己的網頁效能到底如何,可以使用lighthouse快速生成一個網頁效能的檢測報告。
移動端使用者體驗隨著JavaScript阻塞互動事件超過14秒以上,逐漸消失。
導致移動端上面程式碼阻塞時間的主要原因是移動端CPU的效能以及網路狀況。
這裡顯示的中國並不是4G覆蓋率非常低,而是沒有資料,但是可以看到西歐、北美等地區的4G覆蓋率也只是60%~80%之間,並不能夠達到基本全覆蓋,所以為了這部分3G使用者的使用者體驗,縮減JavaScript壓縮後檔案的大小也是必然的。
並且移動端裝置的效能也是瓶頸之一,智慧手機的普及率雖然比較高,但是質量參差不齊,許多移動端裝置還停留在1G甚至512RAM的情況下。
大部分網際網路公司都會採用兩套web來實現移動端和桌面端,移動端採用高壓縮的頁面,來減少網路時間和載入時間。
JavaScript存在成本
如果頁面有著過多的指令碼,那麼就需要考慮code-spliting來分開bundle程式碼,或者通過tree-shaking來減少JavaScript的包袱。
目前我們的業務專案採用React+Node.js的SSR來進行SEO優化和首屏效能提升。我們的JS Bundle中有著很多的JavaScript庫程式碼:
react
&&react-dom
等客戶端框架;- 大型SPA可選的狀態管理解決方案:
mobx
、vuex
、redux
、rxjs
; - ES6、ES7等polyfills,為瀏覽器廠商還債;
- @music等元件庫,包括Utils元件以及UI元件。
即使已經完成了code split,首屏載入中,上面的這些庫也會有一大部分被載入進來,造成整個專案的JavaScript冗餘。
整個頁面在載入的時候,有著幾個重要的時間節點。
是否開始有內容顯示在頁面上了。也就是使用者能夠感受到自己得到了響應;
是否有完整的內容顯示在頁面上,也就是可互動的內容已經顯示了出來;
是否可以開始進行互動了,也就是意味著使用者能夠開始對頁面進行正常操作。
前兩個階段在服務端渲染的情況下,大部分進行的還是頁面的render操作,render是沒有太多辦法來對其進行加速的。所以為了提升互動效果,第三階段是必須著力解決的。不能夠產生互動的主要原因是指令碼還沒有載入完成,導致了頁面阻塞。加速載入和執行,通過減少JavaScript包的大小是可行的方法。
另外一種方法是通過SSR,為使用者提供更快的首屏渲染速度,並且在之後,將JavaScript注入到頁面當中。
通過<script>
標籤等主程式載入過多的JavaScript會造成阻塞問題,而採用Web Worker
程式或者快取來進行頁面指令碼的執行能夠得到更短的阻塞時間。
我們估測了Google News的移動端可互動時間,在高階裝置上大約是7秒左右,而低端裝置則達到了55秒。
中低端裝置的JavaScript執行速度遠遠比我預期的要長很多,由於生活和工作環境中較少接觸這類裝置,所以這類使用者裝置的比例是需要進行埋點獲取的,如果這類裝置的比例較高(在國內是比較有可能發生的),那麼就需要為這類使用者進行JavaScript的削減或者壓縮。
Pinterest將打包後的JavaScript從2.5MB壓縮到了小於200KB,響應時間從23秒降低到了5.6秒,這樣帶來的直接結果就是,他們的收入提升了44%而註冊量提升了753%Orz,移動端的周活提升了103%。
我們需要做的重點是防止JavaScript成為整個網站的瓶頸。
需要記住的是,如果想要讓JavaScript變得更快,那麼需要做到下面幾點:
- 下載快
- 解析快
- 編譯快
- 執行快
Parse/Compile
上面是各大網站在V8引擎上面的指令碼執行時間圖,解析和編譯階段佔了總時間的大約10%~30%,在Chrome 66中,V8可以在後臺執行緒編譯程式碼,可以將編譯時間減低到大約20%左右,但是也很少見到能夠在50ms之內編譯完的JavaScript程式碼。
另一個要注意的事情就是,JavaScript的大小並不完全意味著它的時間消耗,一個200KB的圖片和一個200KB的JavaScript所佔用的時間是完全不同的。
兩者的下載時間應該是差不多並且和大小強相關的,但是圖片需要解碼,柵格化以及繪製到螢幕上,而JavaScript程式碼包需要解析,編譯以及執行。JavaScript的時間消耗基本是要大於圖片的,這個差距也取決於裝置的CPU效能。
根據上述內容,**在進行效能測試的時候,儘量讓環境惡劣起來,不要使用高速的網路環境以及高效能裝置來進行測試。**因為使用者裝置和環境的均值可能是你想象不到的。
可變性會殺死使用者體驗,高效能裝置可能會變慢,高速網路也可能會變慢,可變性最終會讓所有事情都變慢。
可變性需要讓開發人員降低開發時的基準線,來保證每一個使用者的體驗。如果你的團隊能夠通過一些策略來獲知所有訪問你的站點的使用者環境,那麼可以很方便地對於站點的效能相容性進行測試。在測試的時候,採用使用者中具有代表性的網路和裝置環境來進行測試。
It's important to know your audience.
- 對於網路,低端網路環境需要更小的JavaScript bundle。這就要求減少程式碼的冗餘、縮小程式碼體積、並且進行壓縮;
- 對於裝置,做好對於重複訪問資料的快取工作,解析時間對於低端裝置來說是最重要的。
當我們的站點越來越依賴於JavaScript的時候,我們有時候就會為了不容易看見的傳送到客戶端的程式碼付出代價。
如何傳送更少的JavaScript
這一點的關鍵在於如何傳送最少限度的JavaScript到客戶端,並且能夠保證使用者的正常體驗。Code-splitting是其中的重點。
目前來說,Code-splitting常用的方法就是在bundle程式碼的時候,僅僅返回當前路由對應的相關程式碼,而不返回整個龐大的程式碼包。對於路由的切分以及庫的引入來說,這一個原則至關重要。無論什麼理由,都儘量不要將其他路由的程式碼注入到當前訪問的路由當中。路由的懶載入是decrease你的JavaScript載入速度的重點。
import Loadable from 'react-loadable';
const LoadableOtherComponent = Loadable({
loader: () => import('./OtherComponent'),
loading: () => <div>Loading...</div>
});
const MyComponent = () => {
<LoadableOtherComponent />
};
複製程式碼
在React中新增code-splitting可以通過React Loadable進行,這個庫是一個HOC,可以動態載入需要的React元件,而不會在首次渲染的時候就將所有的頁面元件都載入進來,即使它暫時不會被使用。
現在也有很多庫可以幫助你來定位自己的程式碼包,來幫助你從程式碼層面減少JavaScript程式碼的長度。比如webpack bundle analyzer,source map explorer,bundle buddy。這些工具會審視你的程式碼,並且找到其中的冗餘,大型庫以及一些未使用的依賴。
打包審查可以著重於一些大型依賴,或者是給你一個較輕的低位替代。
措施,優化,監控以及重複
如果你不確定自己的工程程式碼是否存在這些問題,通過LightHouse可以審查你的站點。
cnpm install -g lighthouse
lighthouse http://yoursites.com
複製程式碼
快速生成一份站點的效能審查報告。
雲音樂主站的審查報告大概是這樣的,我們的移動端主站大概需要3秒左右的時間能夠得到互動響應。
由於這篇文章是從我的桌面上淘出來的,所以圖似乎都掛掉了。。有興趣的可以自己去跑一下雲音樂主站的程式碼哦!
Code Coverage是一個用來發現你的頁面中的未使用JavaScript以及CSS的DevTools。使用這個工具可以看到頁面中有多少程式碼影響了載入效能,並且這些程式碼讓你付出多少時間的代價。
這是主站測試環境下的程式碼覆蓋率,可以發現libs檔案基本上有一半都未在使用。而CSS更加誇張,95%的程式碼都沒有使用過。
PRPL原則
PRPL(Push、Render、Precache、Lazy-Load)模式適用於盡力將每一個單獨路由的程式碼拆開,然後利用
service worker
來pre-cacheJavaScript程式碼,這些懶載入的程式碼是與當前路由強相關的路由的邏輯程式碼。
也就是說,我們在進行路由載入的時候,僅僅載入一個純淨的路由頁面。當這個路由頁面渲染完畢之後,我們通過一個路由相關的list來將其他與當前路由有跳轉規則的路由頁面載入進來,通過service worker
來在後臺執行緒進行懶載入與解析。並且根據我們當前的環境,來進行優雅降級,如果裝置不支援後臺執行緒,那麼就採用主執行緒來進行載入。
效能預算
效能預算是保證所有開發人員在一個頻道的關鍵,效能預算定義了一個常量,來讓團隊有著共同的效能目標。
效能預算一般包括下面幾個部分:
- Milestone timings(里程碑時間?):這個時間一般基於載入頁面時候的使用者體驗,比如可以開始互動的時候。這個時間一般是頁面完成載入的精確時間。
- Quality-based metrics:基於純粹的值,比如JavaScript的大小,HTTP請求的數量,這個值主要關注瀏覽器體驗。
- Rule-based metrics:通過LightHouse或者其他頁面測試工具得到的評分。
由於現在的大型專案總都是由多個開發人員一起進行開發的,這些開發人員對於頁面的效能並沒有一個統一的標準,通過上面三個標準,每個人在加入了自己的程式碼之後就可以對於效能進行測試,來確認自己是否影響到了整個專案的效能預算。比如自己的庫導致了團隊的JavaScript程式碼超量。這時,團隊的人就需要根據情況來削減自己的程式碼量。
下面的有點可怕,就放原文了:
Here’s an action plan for performance:
Create your performance vision. This is a one-page agreement on what business stakeholders and developers consider “good performance”
Set your performance budgets. Extract key performance indicators (KPIs) from the vision and set realistic, measurable targets from them. e.g. “Load and get interactive in 5s”. Size budgets can fall out of this. e.g “Keep JS < 170KB minified/compressed”
Create regular reports on KPIs. This can be a regular report sent out to the business highlighting progress and success.
在GIT合併的時候設定LightHouse的檢測規則,如果導致了LightHouse評分降低,則blocked該次合併。在對於效能極致要求的業務情況下,這個方法的確能夠有效地提升業務程式碼的質量。
Get fast, Keep fast
效能不是一蹴而就的,許多細小的變動都可能會帶來大的收益。