移動web效能優化從入門到進階

呂小鳴發表於2019-03-21

關於前端效能優化相關的技術知識,網上隨便搜一些就有很多,本文將系統性的從初級到高階的思路,總結移動前端效能優化各個方面的相關技術點,內容來自筆者以往經驗的總結,希望讀者可以花些時間看看。

在目前大多數剛從事前端開發,或者是正在學習前端開發的同學來說,效能優化對於他們可能還比較遠,但是脫穎而出,拉開差距的點,往往就在與效能優化,和理論知識不同,效能優化往往來自日常的工作經驗中總結而來,也是目前大廠面試前端必問的知識點,所以重要性就不言而喻了。

一,入門篇

首先,重要的事情說三遍:

移動web效能優化原理知識同樣適用於PCweb端! 移動web效能優化原理知識同樣適用於PCweb端! 移動web效能優化原理知識同樣適用於PCweb端!

1.資源合併與壓縮

為什麼要壓縮

不同於大部分放在服務端的後臺程式碼,前端所有的檔案程式程式碼都是要通過瀏覽器下載下來執行使用,這就牽扯到網路和請求延時,所以前端檔案的精簡和壓縮決定了前端效能的第一步。

介於目前的前端框架類庫,webpack,vue-cli等等,已經可以直接將這一步操作整合到我們的系統專案中了,可以直接檢視各個框架的文件來進行配置,單純的使用原生技術,可以參考下面:

html的壓縮

HTML程式碼壓縮就是壓縮這些在文字檔案中有意義,但是在HTML中不顯示的字元,包括空格,製表符,換行符等,還有一些其他意義的字元,如HTML註釋也可以被壓縮。

  • Nodejs的html-minifier
  • 線上壓縮工具,站長工具等等。

CSS和JavaScript檔案的壓縮

JavaScript壓縮,主要是去除多餘的換行和空格等等,對於語法來說,JavaScript可以選擇混淆壓縮和非混淆壓縮,無論哪種壓縮都是為了減少JavaScript的檔案大小,當然出於前端程式碼保護來看,混淆壓縮會大大破壞原有的閱讀邏輯,增加壓縮比,從而給程式碼新增一層保護。 CSS壓縮,同理是去除多餘的換行和空格等等,由於CSS檔案的特殊性暫時無法實現混淆壓縮,壓縮主要是將大量的換行去除,可以減少不少的檔案大小。

  • Nodejs的uglifyjs2是一個強大的JavaScript壓縮庫。
  • Nodejs的clean-css是一個強大的CSS壓縮庫。
  • 線上壓縮工具,站長工具等等。

圖片的壓縮

對於常見的前端專案,關於圖片的使用,主要有以下兩種:

  • 固定圖示,背景,按鈕icon等等,這些圖片有一個特點就是固定和使用者無關,一般是放在原始碼包裡面,由前端程式碼直接引入。
  • 人物頭像,文章配圖,內容圖片等等,這些非固定圖片一般由使用者上傳,有很強的使用者性,這些圖片一般放在CDN上,前端通過連結請求。
  1. 對於固定圖片,推薦tinypng.com/線上壓縮之後再進行引入,支援png,jpeg型別的圖片,屬於有失真壓縮,去除圖片一些不必要的後設資料,把相似畫素的24bit位用8bit位來表示,肉眼很難區分,壓縮率70%。
    圖片描述
  2. 採用CSS雪碧圖:把你的網站用到的一些圖片整合到一張單獨的圖片中: 優點:減少HTTP請求的數量(通過backgroundPosition定位所需圖片)。 缺點:整合圖片比較大時,載入比較慢(如果這張圖片沒有載入成功,整個頁面會失去圖片資訊)。
  3. 對於非固定圖片,常見的優化壓縮主要有以下幾種原則: 優先使用壓縮率高的jpeg型別圖片,缺點是不支援透明。 有條件的話使用webP(一種Google開發的新型別)型別圖片是最佳選擇,相比於jpeg,有更小的檔案尺寸和更高的影象質量。

資源合併

在前端編碼的時候將css、js等靜態資原始檔合併壓縮之外,我們還可以在頁面中將多個css、js的請求合併為一個請求。檔案的合併帶來的是http請求數的減少,尤其是在移動端,每一個http請求帶來的是慢啟動三次握手連線建立,所以資源的合併是由為重要的,合併和不合並對比:

圖片描述

2.瀏覽器載入原理優化

HTML頁面載入渲染的過程:

圖片描述

根據上圖我們來屢一下整個流程:

  1. 當瀏覽器從伺服器接收到了HTML文件,並把HTML在記憶體中轉換成DOM樹,在轉換的過程中如果發現某個節點(node)上引用了CSS或者 IMAGE,就會再發1個request去請求CSS或image,然後繼續執行下面的轉換,而不需要等待request的返回,當request返回 後,只需要把返回的內容放入到DOM樹中對應的位置就OK。
  2. 但當引用了JS的時候,瀏覽器傳送1個js request就會一直等待該request的返回。
  3. 因為瀏覽器需要1個穩定的DOM樹結構,而JS中很有可能有程式碼直接改變了DOM樹結構,瀏覽器為了防止出現JS修改DOM樹,需要重新構建DOM樹的情況,所以 就會阻塞其他的下載和呈現。 那麼如何解決和避免阻塞的問題呢,我們通過測試程式碼分別測試不同情況下引入js和css的問題如下:
<!DOCTYPE html>
<html>
  <head>
    <title>test</title>
      <link rel=”stylesheet” type=”text/css” href=”stylesheet.css” media=”screen”>
      <link rel=”stylesheet” type=”text/css” href=”page-animation.css” media=”screen”>
      <script type =”text/javascript” >
          var f = 1;
          f++;
      </script >
  </head>
  <body>
    <img src=”download-button.png”>
  </body>
</html>
複製程式碼

測試過程省略,可以參考這裡,我們可以得到如下的結論:

  • 瀏覽器存在併發載入:資源請求是併發請求的。
  • 瀏覽器中可以支援併發請求,不同瀏覽器所支援的併發數量不同(以域名劃分),以Chrome為例,併發上限為6優化點: 把CDN資源分佈在多個域名下。
  • css 在head中通過link引入會阻塞頁面的渲染,處於頁面樣式,我們必須這樣放置。
  • 直接通過<script src>引入的外部js會阻塞後面節點的渲染,所以外部js儘量放在body底部。
  • 在head裡面儘量不要引入js。
  • 如果要引入js 儘量將js內嵌。
  • 把內嵌js放在所有link引入css的前面。
  • 對於要阻塞後續內容的的外部js<script src>,需要增加defer來解決。

3.快取優化

如果一個H5頁面沒有利用任何快取,那麼這個頁面將沒有任何存在的意義。

從從HTTP協議快取,到瀏覽器快取,再到APP Cache,一直在最近比較火的Service worker,我們可以選擇多種的快取方式,入門基本來說說HTTP協議快取:

圖片描述

強快取:Expires&Cache-Control

當瀏覽器對某個資源的請求命中了強快取時,返回的HTTP狀態為200,在chrome的開發者工具的network裡面 size會顯示為from disk cache,這種情況下是不用傳送任何請求,如下圖

圖片描述

  • Expires:指定了在瀏覽器上緩衝儲存的頁距過期還有多少時間,等同Cache-control中的max-age的效果,如果同時存在,則被Cache-Control的max-age覆蓋。
  • Cache-Control:
    • public:響應被快取,並且在多使用者間共享。
    • private:預設值,響應只能夠作為私有的快取(e.g., 在一個瀏覽器中),不能再使用者間共享;
    • no-cache:響應不會被快取,而是實時向伺服器端請求資源。
    • max-age:數值,單位是秒,從請求時間開始到過期時間之間的秒數。基於請求時間(Date欄位)的相對時間間隔,而不是絕對過期時間;

協商快取:Last-Modified&Etag

當瀏覽器對某個資源的請求沒有命中強快取,就會發一個請求到伺服器,驗證協商快取是否命中,如果協商快取命中,請求響應返回的http狀態為304並且會顯示一個Not Modified的字串,比如你開啟京東的首頁,按f12開啟開發者工具,再按f5重新整理頁面,檢視network,可以看到有不少請求就是命中了協商快取的:

圖片描述

  • Last-Modified/If-Modified-Since:本地檔案在伺服器上的最後一次修改時間。快取過期時把瀏覽器端快取頁面的最後修改時間傳送到伺服器去,伺服器會把這個時間與伺服器上實際檔案的最後修改時間進行對比,如果時間一致,那麼返回304,客戶端就直接使用本地快取檔案。
  • Etag/If-None-Match:(EntityTags)是URL的tag,用來標示URL物件是否改變,一般為資源實體的雜湊值。和Last-Modified類似,如果伺服器驗證資源的ETag沒有改變(該資源沒有更新),將返回一個304狀態告訴客戶端使用本地快取檔案。Etag的優先順序高於Last-Modified,Etag主要為了解決
  • Last-Modified 無法解決的一些問題。
    • 檔案也許會週期性的更改,但是他的內容並不改變,不希望客戶端重新get;
    • If-Modified-Since能檢查到的粒度是s級;
    • 某些伺服器不能精確的得到檔案的最後修改時間。

4.懶載入與預載入

懶載入對於移動web端,尤其是最常見的滾動載入場景是一項非常重要的優化措施。而預載入則常常應用於多tab場景的頁面,讓使用者更快的看到開啟的下一個頁面。

懶載入

  • 圖片進入可視區域之後請求圖片資源。
  • 對於電商等圖片很多,頁面很長的業務場景適用。
  • 減少無效資源的載入。
  • 併發載入的資源過多會會阻塞js的載入,影響網站的正常使用。 img src被設定之後,webkit解析到之後才去請求這個資源。所以我們希望圖片到達可視區域之後,img src才會被設定進來,沒有到達可視區域前並不現實真正的src,而是類似一個1px的佔位符。

預載入

  • 圖片等靜態資源在使用之前的提前請求。
  • 資源使用到時能從快取中載入,提升使用者體驗。
  • 點選操作前預先載入下一屏資料。

ok,讀到這裡,對於一些剛入門的前端玩家,或者是還在學習前端的同學,掌握了上面的入門級效能優化基礎知識,才能算是基本的合格,真正更進一步的優化,更適合移動端web的效能點,可以參考進階版:

二,進階篇

1.資源合併與壓縮

啟用GZIP

gzip是GNUzip的縮寫,最早用於UNIX系統的檔案壓縮。HTTP協議上的gzip編碼是一種用來改進web應用程式效能的技術,web伺服器和客戶端(瀏覽器)必須共同支援gzip。目前主流的瀏覽器,Chrome,firefox,IE等都支援該協議。常見的伺服器如Apache,Nginx,IIS同樣支援gzip。 gizp流程

  • 瀏覽器請求url,並在request header中設定屬性accept-encoding:gzip。
  • 伺服器支援gzip,response headers返回包含content-encoding:gzip。
  • 開啟gzip可以達到80%的壓縮率,即1MB的檔案下載下來只需要200K,大大減少傳輸效率,是一項非常重要的資源壓縮手段。

Nginx中開啟gzip:

圖片描述

升級HTTP/2.0

HTTP/2是HTTP協議自1999年HTTP 1.1釋出後的首個更新,主要基於SPDY協議(是Google開發的基於TCP的應用層協議,用以最小化網路延遲,提升網路速度,優化使用者的網路使用體驗)。 優化原理: 根據上文中說的資源合併問題,瀏覽器可以同時建立有限個TCP連線,而每個連線都要經過慢啟動三次握手連線建立,HTTP1.1為了解決這個問題推出了keep-alive,即保持連線不被釋放,但是真正的這些連線下載資源是一個線性的流程:一個資源的請求響應返回後,下一個請求才能傳送。這被稱為線頭阻塞,為了徹底解決此問題,HTTP2.0帶來了多路複用

圖片描述
HTTP2.0的其他新特性也有助於頁面的開啟速度:

合併資源 vs 並行載入資源?

現在回過頭來探討一下上文說的資源合併問題,有了HTTP2.0之後,我們是否還需要合併資源,目前看需要遵循下面的原則:

  • 停止合併檔案 在HTTP/1.1中,CSS,JavaScript被壓縮到了一個檔案,圖片被合併到了一張雪碧圖上。合併CSS、JavaScript和圖片極大地減少了HTTP的請求數,在HTTP/1.1中能獲得顯著的效能提升。 但是,在HTTP/2.0中合併檔案不再是一個好的辦法。雖然合併依然可以提高壓縮率,但它帶來了代價高昂的快取失效。即使有一行程式碼改變了,整個檔案就要重新打包壓縮,瀏覽器也會強制重新載入新的檔案。

  • 儘量不要在HTML裡內聯資源 非特殊的程式碼(rem適配程式碼,上報程式碼等)之外,儘量不要使用內聯資源,在極端情況下,這確實能夠減少給定網頁的HTTP請求數。但是,和檔案合併一樣,HTTP/2優化時你不應該內聯檔案。內聯意味著瀏覽器不能快取單個的資源。如果你將所有頁面使用的CSS宣告嵌入了每一個HTML檔案,這些檔案每次都要從服務端獲取。這導致使用者在訪問任何頁面時都要傳輸額外的位元組。

  • 合併域名 拆分域名是讓瀏覽器建立更多TCP連線的通常手段,瀏覽器限制了單個伺服器的連線數量,但是通過將網站上的資源切分到幾個域上,你可以獲得額外的TCP連線,但是每個拆分的域名都會帶來額外的DNS查詢、握手,新連線的建立,根據HTTP2.0多路複用的原則:HTTP2採用多路複用是指,在同一個域名下,開啟一個TCP的connection,每個請求以stream的方式傳輸,域名的合併可以帶來更多的多路複用,如下圖在chrome的Network皮膚中檢視HTTP2.0,注意protocol和ConnectID相同則表示啟用複用:

    圖片描述

合理使用icon類圖片base64化

<img src="data:image/gif;base64,R0..."/>
複製程式碼

在頁面使用的背景類圖片icon類圖片,不多且比較小的情況下,可以把圖片轉成base64編碼嵌入到html頁面或者CSS檔案中,這樣可以減少頁面的HTTP請求數。需要注意的是,要保證圖片較小,一般超過5kb的就不推薦base64嵌入顯示了。為什麼是5kb?。 同時,採用Webpack的url-loader可以幫我們在不影響程式碼可讀性的情況下,解決base64字串問題。

Icon Font

IconFont技術起源於Web領域的Web Font技術,它是把一些簡單的圖示製作成字型,然後讓圖示變成和字型一樣使用,Icon 的設計和使用在近幾年的發展中,也經歷了由當初的 img 方案 到現如今的 svg 方案,有以下優點:

  • 字型是向量的,所以可以隨意改變大小。
  • 因為它是字型,所以所有字型的css都可以使用,比如font-size,color,background,opacity等。
  • 減少圖片請求數。
  • iconfont沒有相容性問題,IE6,Android2.3都能夠相容。

2.瀏覽器載入原理優化

首屏資源優化

  • 剝離首屏資源 首屏的快速顯示,可以大大提升使用者對頁面速度的感知,因此應儘量針對首屏的快速顯示做優化,基於聯通3G網路平均338KB/s(2.71Mb/s),所以首屏資源不應超過1014KB,剝離首屏需要的資源,非首屏的資源單獨合併,採用懶載入。這個原則適用上文的資源合併和載入中的場景。

  • 按需載入 將不影響首屏的資源和當前螢幕資源不用的資源放到使用者需要時才載入,可以大大提升重要資源的顯示速度和降低總體流量,對於移動web端常見的多tab頁面,Webpack的Code Splitting幫助我們更加便捷實現按需載入。

  • 非首屏圖片Lazyload 不用多說,在目前流量費用還算比較高昂的情況下,幫助使用者節省更多的流量可以避免使用者的投訴,為了保證頁面內容最小化,加速頁面渲染,儘可能節省首屏網路流量,頁面中的圖片資源推薦使用懶載入實現,在頁面滾動時動態載入圖片。

使用CDN

CDN是將源站內容分發至最接近使用者的節點,使使用者可就近取得所需內容,提高使用者訪問的響應速度和成功率。解決因分佈、頻寬、伺服器效能帶來的訪問延遲問題,適用於站點加速、點播、直播等場景。 對於web頁面來說,將專案的js,css等靜態資源存放在CDN是一個重要的優化手段,加入所有資源統一打包放在同一個域名下,很難達到使用者就近獲取的優勢(目前最佳實踐是html頁面採用一個域名,靜態資原始檔採用CDN域名),所謂靜態資源即是可以被瀏覽器快取的資源,而對於html頁面,由於是js和css等連結的入口,通常不採用快取。常用的阿里雲CDN騰訊雲CDN都有開放介面,開發者可以按需選擇。

預載入

此預載入主要分為兩個部分,一種是採用原生瀏覽器支援的API來對頁面的一些資源進行預先拉取或者載入,另一種是通過自己寫邏輯來載入一些重要的資源,立即下面內容的前提是要立即目前移動web常見的hybrid架構,webview外殼+H5頁面:

圖片描述

  • DNS預解析(dns-prefetch) DNS 作為網際網路的基礎協議,其解析的速度似乎很容易被網站優化人員忽視。現在大多數新瀏覽器已經針對DNS解析進行了優化,典型的一次DNS解析需要耗費 20-120 毫秒,減少DNS解析時間和次數是個很好的優化方式。DNS Prefetching 是讓具有此屬性的域名不需要使用者點選連結就在後臺解析,而域名解析和內容載入是序列的網路操作,所以這個方式能 減少使用者的等待時間,提升使用者體驗 。

    <link rel="dns-prefetch" href="//haitao.nos.netease.com">
    複製程式碼
  • Preload 和 Prefetch 兩者都是以<link rel="preload"> 和 <link rel="prefetch">作為引入方式。 Preload 一個基本的用法是提前載入資源,告訴瀏覽器預先請求當前頁需要的資源,從而提高這些資源的請求優先順序,載入但是不執行,佔用瀏覽器對同一個域名的併發數:

    <link rel="preload" href="a.js" as="script" onload="preloadLoad()">
    複製程式碼

    Prefetch 一個一本用法是瀏覽器會在空閒的時候,下載資源, 並快取起來。當有頁面使用的時候,直接從快取中讀取。其實就是把決定是否和什麼時間載入這個資源的決定權交給瀏覽器。

    <link rel="prefetch" href="a.js">
    複製程式碼

    遺憾的是對於這兩個介面,移動端的瀏覽器支援性很不好,這也是沒有普遍推廣開勇的原因。

    圖片描述
    圖片描述
    圖片描述
    什麼時候使用Preload,什麼時候使用Prefetch可以總結如下: 對於當前頁面很有必要的資源使用 preload,對於可能在將來的頁面中使用的資源使用 prefetch。 關於Preload 和 Prefetch可以參考這裡,另外還有PrerendersubresourcePreconnect屬性,由於目前能支援到這些屬性的機型太少,這裡就不在贅述了。

  • 業務邏輯的預載入 關於業務邏輯的預載入,在這裡我可以舉一個微信小程式的例子。小程式主要分為渲染層和邏輯層,邏輯層有iOS或者Android的JavaScript core來執行,渲染層由各自的webview元件負責渲染。我們使用者實際體驗到的UI還是跑在我們的webview裡面,這個和大多數H5頁面的渲染用的是一個元件。但是為什麼我們體驗小程式會比H5頁面要快很多?尤其是新開頁面時?

    圖片描述
    小程式在啟動時,會預先載入所有頁面邏輯程式碼進記憶體,在 a頁面跳轉至 b頁面 時,可以在記憶體中直接執行而無需在傳送資源請求,a頁面的邏輯程式碼 Javascript 資料也不會從記憶體中消失。b頁面甚至可以直接訪問 a頁面中的資料,整個壞境在一個大的上下文中。 當然這裡你可能會有疑問?假如使用者不會進入page2,那載入page2的邏輯程式碼豈不是浪費?這裡就會牽扯到一個使用者行為預測的問題,在小程式的架構中,整個邏輯程式碼是統一在一個包裡,微信是統一將這些檔案下載並載入到記憶體中,這可能會涉及到一些浪費,但是對於提速來講收益大於弊端的。當然小程式頁提供出分包策略來優化這些問題。 藉助小程式的思路,我們的移動web同樣適用這種預載入優化邏輯: 1. 預載入資源:在多tab的單頁應用中,我們可以在使用者開啟首屏之後,預先載入其他tab的資源。例如使用者進入時在推薦tab,這時就可以預先載入訂單,我的 這兩個tab的資源了,當使用者點選訂單時,頁面的展現就會快一些。
    圖片描述
    2. 預載入資料:預載入資料的時機最好是在空閒時,什麼是空閒時呢?我們分析一下開啟一個H5頁面的流程:
    圖片描述
    從圖上可以看到,利用閒時可以做的事情有很多,預載入資料是一個典型的優化手段,提前把新頁面所需要的資料載入好,在新頁面開啟後,可以直接用資料來進行渲染,當然這裡涉及到的跨頁面資料通訊,我們可以利用localStroage來實現。 3. 預載入webview 我利用閒時來做更多事情的前提是閒時夠長,但這本書也不是一個很好的現象,儘量的減少閒時,也是我們需要做的一項優化,例如我們來減少webview的載入時間,這就需要提前載入webview,此項優化大多是由native端來完成:

    • 在APP啟動後,就提前在記憶體中將webview載入好,而不是等到點選進入web頁面時才去載入。
    • 建立一個webview的複用池,例如最多隻存在3個webview,每次從池子裡獲取webview,達到複用的目的。

3.合理利用快取

上文說了瀏覽器快取的基礎知識,既然是基礎,那就說明必須掌握,下面來說一些進階篇的利用快取來優化頁面:

妙用localStorage

HTML5 LocalStorage可以看做是加強版的cookie,資料儲存大小提升,有更好的彈性以及架構,可以將資料寫入到本機的ROM中,還可以在關閉瀏覽器後再次開啟時恢復資料,以減少網路流量,日常使用localStorage來優化我們的頁面大概有以下幾種場景:

  • 快取一些非實時更新的變數,例如某些閃屏的標誌位資訊,地理位置資訊等等,取用方便,即存即用。
  • 使用localStorage快取Js和css文的,為了提升頁面的開啟速度,或者是頁面可以離線使用,有些頁面會採用將靜態資原始檔直接快取在localStorage中,當頁面開啟時將內容讀取出並執行,使用此方法確實可以減少http請求,提速頁面。
  • 在一些跨webview通訊的場景中,localStorage是相容性最好的資料通訊方案,例如預載入的資料可以快取在localStorage中,來實現各個頁面的webview資料共享。
  • 需要注意的是,localStorage並不是無限大的,針對每個域名,PC端瀏覽器給localStorage分配的容量大概4.5m-5m,移動端類似微信等等的瀏覽器大概容量是2.5m-3m參考這裡。所以在使用時需要做好異常捕獲,讓localstorage超出容量時,是無法在進行插入並報錯,如果對容量有更高的要求,可以參考使用indexeddb。需要注意的是indexeddb的相容性卻不是很好,android4.4之前以及iOS7以前都無法使用。

老生常談離線包

離線包技術可以說是並不算很新的技術了,各個業務都有在使用,也都有自己的一套hybrid離線包系統,關鍵點在於離線包的打包,同時對檔案加密/簽名,更新離線包(增量) ,安全教研以及容錯機制等等,在這裡列舉一些大廠的離線包方案來參考:

Service Worker探索

提到快取,那就不得不提近幾年比較火的Service Worker了:

圖片描述
作為一個比較新的技術,大家可以把 Service Worker 理解為一個介於客戶端和伺服器之間的一個代理伺服器。在 Service Worker 中我們可以做很多事情,比如攔截客戶端的請求、向客戶端傳送訊息、向伺服器發起請求等等,其中最重要的作用之一就是離線資源快取。 Service Worker的主要複雜點在於不斷地對快取策略的調整,筆者在這裡就不過多展開,可以參考一下淘寶Service worker實戰

4.Nodejs服務端渲染(SSR)優化首屏時間

在前後端分離之後,後端語言的模板功能被弱化,整個頁面的渲染基本上都由前端 js 動態渲染,但這樣對於一些應用來說是有缺陷的。比如需要 SEO 的,需要開啟頁面不用等待就能看到頁面的,另外前端頁面展示過度依賴js和css邏輯執行,在極端情況或者網路較差,手機效能低下(尤其在低端Android機型較為明顯)時,白屏時間較長,這時服務端渲染便應用而生,至於為什麼是Nodejs,作為一個前端,難道還要用Java麼。。? 為什麼會有服務端渲染? 如果你說服務端渲染和早期web框架,例如SSH,JSP servlet,PHP等等一樣的話,那我只能說呵呵,目前的服務端渲染和早期的框架是有本質區別的:

  • Web 2.0時代最大的思想革命本質不是前後端分離,而是把網頁當作獨立的應用程式(app)。建立在前後端分離的基礎上,後端只負責提供資料json格式,前端還是負責頁面互動邏輯,大多數的服務端渲染採用Nodejs層來進行資料組裝,html拼接。
  • 重點在首屏!!首屏時間的優化,移動網際網路時代的爆發,使用者對網頁效能的要求越來越高,但畢竟基於3G,4G網路,讓使用者更快的看到頁面就能挽留更多的潛在商機。服務端只負責首屏的頁面渲染,真正過了首屏,大多數的業務邏輯,頁面互動,還是需要有單獨的前端來實現的。

如何實現? 如果你的專案用的是React或者是vue,那麼下面兩個現場的開源框架是不錯的選擇。

當然,你也可以自己實現一套自己的服務端渲染框架,一般需要關注這些問題:

  • 實現自定義的Node端的window上下文物件Cookie & Session等。
  • 遠端資料的獲取,一般採用Nodejs的http模組。
  • React採用ReactDOMServer呼叫renderToString(),Vue採用vue-server-renderer呼叫renderToString()。
  • Node端記憶體洩露和控制等問題

程式碼同構(isomorphic) 使用Nodejs的服務端渲染的一大優勢就是程式碼同構,這使得一個專案可以分別部署成走線上正常前端渲染版本,和走服務端渲染版本,這樣可以更好的做到容災機制,當任何一種分之掛掉之後,可以直接走另一個版本,提高穩定性。這也同構的魅力所在!因為在同構直出宕掉的時候,還有前端渲染頁面可以提供正常的服務。

圖片描述

取捨 雖然說服務端渲染這類優化確實可以提升一定的頁面首屏時間,但是也是需要成本的,在前端開發接管了Node作為中間層時,需要額外的機器資源部署,並且一旦接觸到後端,容災機制,記憶體管理等效能指標都需要關注,這對於當前的業務系統架構可能需要有一定的調整,所以還是要斟酌來使用。

5.渲染優化

終於回到我們前端的老本家了,如果說前面的優化都是在框架,邏輯層面的優化,或者是參考後端,客戶端的優化思路,那麼真正涉及到UI渲染的優化才是我們作為前端工程師的立身之本了。

  • 何為渲染優化?

    拋開首屏加速,真正讓使用者體驗web頁面的另一個很重要的部分就是使用者行為互動了,這包括使用者的點選相應滾動流暢度動畫是否卡頓流暢度等等,這些關於使用者互動性的優化在已往的PC端可能不是很被重視,因為PC瀏覽器的效能要遠遠大於手機端,但是到了移動web就不一樣了,使用者都希望移動web能有PC端一樣的效能。

  • 為什麼同樣的頁面在iPhone裡總比Android流暢?

    目前主流的Android硬體配置可以說是甩iPhone幾條街了,那為什麼高配置卻得不到好的體驗呢?關鍵兩類機型的作業系統上的優化程度,其中一個原因就是iOS作業系統採用執行率較高的Object-c語言,大部分硬體介面可以直接呼叫和執行,而Android則採用Java語言,因為虛擬機器的存在,雖然跨平臺性提升了,但是通過虛擬機器在和系統硬體互動,執行效率就低了很多,當然這只是其中一個原因。那麼,我們移動web主要優化的群體就是Android機型了。

  • 16ms優化

    目前大多數裝置的螢幕重新整理頻率為60次/秒,每一幀所消耗的時間約為16ms(1000 ms / 60 = 16.66ms),這16ms就是渲染幀的時長,所謂渲染幀是指瀏覽器一次完整繪製過程,幀之間的時間間隔是DOM檢視更新的最小間隔,但實際上,瀏覽器還有一些整理工作要做,因此開發者所做的所有工作需要在10ms內完成。 如果不能完成,幀率將會下降,網頁會在螢幕上抖動,也就是通常所說的卡頓,這會對使用者體驗產生嚴重的負面影響。所以如果一個頁面中有動畫效果或者使用者正在滾動頁面,那麼瀏覽器渲染動畫或頁面的速率也要儘可能地與裝置螢幕的重新整理頻率保持一致,以保證良好的使用者體驗。在這一個間隔內,瀏覽器可能需要做以下事情:

    • 指令碼執行(JavaScript):指令碼造成了需要重繪的改動,比如增刪 DOM、請求動畫等
    • 樣式計算(CSS Object Model):級聯地生成每個節點的生效樣式。
    • 佈局(Layout):計算佈局,執行渲染演算法
    • 重繪(Paint):各層分別進行繪製(比如 3D 動畫)
    • 合成(Composite):合成各層的渲染結果
  • 重繪和重排 在上面瀏覽器需要做的這些事情中,會引發不同程度的重繪和重排,而重繪和重排正式影響流暢的重要因素:

    1. 部分渲染樹(或者整個渲染樹)需要重新分析並且節點尺寸需要重新計算,這被稱為重排。

    2. 由於節點的幾何屬性發生改變或者由於樣式發生改變,例如改變元素背景色時,螢幕上的部分內容需要更新,這樣的更新被稱為重繪。

    重排和重繪代價是高昂的,它們會破壞使用者體驗,並且讓UI展示非常遲緩,但是每次重排,必然會導致重繪,而每次重繪並不一定會發生重排,我們需要在以下幾種場景來減少重排的發生: 當頁面佈局和幾何屬性改變時就需要回流。下述情況會發生瀏覽器迴流:

    1. 新增或者刪除可見的DOM元素。
    2. 元素位置改變。
    3. 元素尺寸改變——邊距、填充、邊框、寬度和高度。
    4. 內容改變——比如文字改變或者圖片大小改變而引起的計算值寬度和高度改變。
    5. 頁面渲染初始化。
    6. 瀏覽器視窗尺寸改變——resize事件發生時。
  • 使用 requestAnimationFrame 提升動畫流暢度的另一個重要因素是讓瀏覽器變得智慧起來,好在瀏覽器給我們提供了這個介面requestAnimationFrame,通過這個API,可以告訴瀏覽器某個JavaScript程式碼要執行動畫,瀏覽器收到通知後,則會執行這些程式碼的時候進行優化,它會確保JS儘早在每一幀的開始執行,實現流暢的效果,而不再需要開發人員煩心重新整理頻率的問題了:

    function animationWidth() {
      var div = document.getElementById('box');
      div.style.width = parseInt(div.style.width) + 1 + 'px';
    
      if(parseInt(div.style.width) < 200) {
        requestAnimationFrame(animationWidth)
      }
    }
    requestAnimationFrame(animationWidth);
    
    複製程式碼
  • 試試requestIdleCallback requestIdleCallback的出現伴隨著React 16 的Fiber特性,他的使用場景是當使用者在做負責互動時,不希望因為一些不重要的任務(如統計上報)導致使用者感覺到卡頓的話,就應該考慮使用了,因為requestIdleCallback回撥的執行的前提條件是當前瀏覽器處於空閒狀態,但是需要注意的是不要在requestIdleCallback操作任何DOM,這違背了這個介面的設計原則。

        requestIdelCallback(myNonEssentialWork);
       function myNonEssentialWork (deadline) {
       
         // deadline.timeRemaining()可以獲取到當前幀剩餘時間
         while (deadline.timeRemaining() > 0 && tasks.length > 0) {
           doWorkIfNeeded();
         }
         if (tasks.length > 0){
           requestIdleCallback(myNonEssentialWork);
         }
       }
    複製程式碼
  • fragment元素的應用 在你使用dominnerHTML方法來插入大量dom節點時,不妨試試fragment,fragment文件片段是個輕量級的document物件,它的設計初衷就是為了完成這類任務——更新和移動節點。文件片段的一個便利的語法特性是當你附加一個片斷到節點時,實際上被新增的是該片斷的子節點,而不是片斷本身。只觸發了一次重排,而且只訪問了一次實時的DOM。

  • 列表滾動優化 長列表滾動在移動端是一種非常常見的互動模式,例如feeds流,圖片流等等,這些列表的滾動流暢度優化對使用者體驗的提升是非常重要的,基於目前的優化思路,藉助dom複用的方案,類似iOS的UITableView或者Android的recyclerview原理,在列表滾動時,只保證視窗區域內的dom節點存在,在有限的dom節點內實現滾動,而不在建立新的節點,在使用者不斷下拉翻頁的過程中,保證整個頁面有限的dom元素來減少記憶體的消耗,原理如下圖:

    圖片描述
    複用的dom:
    圖片描述
    採用這一個方案的前端是藉助瀏覽器的onscroll事件來做邏輯處理,但是問題在於有些機型例如iOS的UIWebview下,onscroll不能實時觸發,這就給優化帶來了難題,由此引發出了模擬滾動:

  • 正常的滾動:我們平時使用的scroll,包括上面講的滾動都屬於正常滾動,利用瀏覽器自身提供的滾動條來實現滾動,底層是由瀏覽器核心控制。

  • 模擬滾動:最典型的例子就是iscroll了,原理一般有兩種:

    • 1).監聽滾動元素的touchmove事件,當事件觸發時修改元素的transform屬性來實現元素的位移,讓手指離開時觸發touchend事件,然後採用requestanimationframe來在一個線型函式下不斷的修改元素的transform來實現手指離開時的一段慣性滾動距離。
    • 2).監聽滾動元素的touchmove事件,當事件觸發時修改元素的transform屬性來實現元素的位移,讓手指離開時觸發touchend事件,然後給元素一個css的animation,並設定好duration和function來實現手指離開時的一段慣性距離。

    結論是如果要採用模擬滾動,可以解決onscroll不實時觸發的問題,從而實現長列表的複用的優化,但是帶來新的問題就是模擬滾動本身也是dom的重繪,增加額外的效能消耗,達到有優化效果並不理想,好在iOS的新版WKwebview解決了onscroll問題,讓開發者有了更好的選擇。

  • 滾動函式防抖 當持續觸發事件時,一定時間段內沒有再觸發事件,事件處理函式才會執行一次,如果設定的時間到來之前,又一次觸發了事件,就重新開始延時。如下圖,持續觸發scroll事件時,並不執行handle函式,當1000毫秒內沒有觸發scroll事件時,才會延時觸發scroll事件。

    function debounce(fn, wait) {
        var timeout = null;
        return function() {
            if(timeout !== null) 
                    clearTimeout(timeout);
            timeout = setTimeout(fn, wait);
        }
    }
    // 處理函式
    function handle() {
        console.log(Math.random()); 
    }
    // 滾動事件
    window.addEventListener('scroll', debounce(handle, 1000));
    複製程式碼
  • 合理使用GPU 動畫卡頓是在移動web開發時經常遇到的問題,解決這個問題一般會用到CSS3硬體加速。硬體加速這個名字感覺上很高大上,其實它做的事情可以簡單概括為:通過GPU進行渲染,解放CPU,我們可以利用GPU的圖形層,將負責的動畫操作放在這個層,如何開啟?

    webkit-transform: translateZ(0);
    複製程式碼

    強制把需要動畫的dom的物件 ,放置在GPU的layout層來快取從而達到任何移動,大小變化都在這個層。 通過開啟GPU硬體加速雖然可以提升動畫渲染效能或解決一些棘手問題,但使用仍需謹慎,使用前一定要進行嚴謹的測試,否則它反而會大量佔用瀏覽網頁使用者的系統資源,尤其是在移動端,肆無忌憚的開啟GPU硬體加速會導致大量消耗記憶體,千萬不要* {webkit-transform: translateZ(0);}

寫在最後

本文在效能優化的基礎上,將移動web的效能點逐步展開和深入,大部分屬於結論性介紹內容,真正實踐還是需要開發人員親身嘗試來得到優化提升,總之, 技術就是在於不斷折騰,願各位在踩坑的道路上一帆風順!

課程推薦

本文向您推薦了慕課網的實戰課程《移動Web APP開發之實戰美團外賣》(立即學習),希望小夥伴們能通過這門課程收穫滿滿,祝大家學習進步。 作者:呂小鳴 來源:慕課網

原文地址:www.imooc.com/article/280…

相關文章