鳥瞰前端 , 再論效能優化

騰訊雲加社群發表於2017-09-20

歡迎大家前往騰訊雲技術社群,獲取更多騰訊海量技術實踐乾貨哦~

導語 : 從事前端有6年+的時間了,從最開始的美工到重構再到偏向js邏輯開發的前端開發,一直在前端這個行業裡面摸索和學習,我現在將自己這些年的一個心得體會來個系統性的梳理寫成一篇關於效能優化的主題文章,希望對大家有點幫助,也歡迎大家提出各種意見和建議。

作者:劉勇剛

前端工程師是一個最近這5-6年才開始慢慢被網際網路公司重視起來的一個職業,可以說是一個新興行業,我用一張簡單的思維導圖帶大家回顧一下前端技術發展的歷程以及未來一個展望:

1.0時代沒什麼說的,html、css打天下的時代,那個時候你會用js開發個計算器就牛逼到不行。2.0時代是最好的時代,新技術、新思想蓬勃發展,堪稱前端的工業革命,前端人員的地位得到了充分認可,門檻也有一定的提升。前端效能優化的涉及點從伺服器到協議再到宿主環境本身都要有比較深刻的認識,業界目前主要還是以雅虎總結出來35條前端效能優化的黃金軍規(www.cnblogs.com/siqi/p/3655… 為參考。今天我想將這些年對前端的效能優化的經驗思考整體來個串燒,帶大家鳥瞰一下前端效能優化目前的一些通行做法以及這麼做的出發點。文章初衷主要是對一些效能優化基礎知識回顧和體系梳理,不對具體技術點做深入分析,點到為止,個人理解不對的地方歡迎各位大神拍磚,拋磚引玉。

引入話題前我還是先從一個老生常談的話題開始:

“從使用者輸入URl到頁面展示給使用者瀏覽器客戶端的過程中發生了什麼?”

這裡用個圖表簡單描述一下幾個步驟:

web優化的目標就是如何讓使用者更快、更簡單易用、更流暢的使用我們的服務,對於前端開發而言就是如何讓我們的資源體量更小、數量更精簡、內容更早呈現、互動更加人性化。

web效能優化有個大家比較公認的二八原則,就是資源從伺服器處理完下發到客戶端的瀏覽器上(上圖第6步)所佔的時間比例大概是整個過程的20%,也就是說伺服器端可以優化的空間的效率提升並不會很明顯,前端效能優化成為web效能優化重點考慮的領域,我下面將會從以下幾個維度去做了自己的一個思考(跟35條軍規有一定重疊)和總結:

一、瀏覽器宿主環境

1、突破單執行緒解析渲染阻塞限制

瀏覽器是一個單執行緒解析模式去解析渲染從伺服器端拿到的html文字,css載入的過程中會對後續的指令碼資源載入造成阻塞,指令碼的載入也會阻塞後續DOM結構的解析造成頁面的留白時間增長,雅虎的35條軍規中有一條就是樣式檔案放在頭部,指令碼檔案放在DOM節點最末尾,減少阻塞。這裡還有幾個針對指令碼檔案的優化:

  • 針對不需要DOM操作(主要考慮是需要操作DOM的指令碼往往需要獲取一些樣式資訊)的Js指令碼可以採用動態建立script的方式載入,動態載入的指令碼不阻塞後續資源的載入。
  • 指令碼檔案載入可以加上defer或者async屬性標識防止阻塞(關於兩者區別可以參考)

2、利用事件冒泡特性

瀏覽器的事件模型的冒泡的特性(瀏覽器事件模型不清楚的自行搜尋瞭解)我覺得是最牛逼的設計之一,解決了瀏覽器因為解析DOM模型不同步導致開發者往DOM物件註冊事件回撥找不到物件的問題。

瀏覽器事件註冊有3個級別定義,DOM 0級事件註冊(利用DOM元素行內事件屬性onclick註冊事件回撥),DOM 1級事件註冊(利用DOM元素物件的onclick API 在外部註冊事件回撥),DOM 2級事件註冊(利用利用DOM元素物件的addEventListner/attachEvent API 在外部註冊事件回撥)。這裡效能優化的建議就是利用DOM2級在目標DOM的父標籤(大部分框架是在body標籤統一註冊事件監聽)註冊回撥,收攏事件監聽入口同時節約了DOM節點引用開銷。

3、避開Cookie效能bug

Cookie是前端作為前後臺登入態校驗最通常用的快取方案,但鑑於瀏覽器在每次都會往同域的任何資源的http請求中自動帶上cookie資訊的情況,這裡有必要進行優化一下,因為像css、js、image這些資源請求是不需要cookie資訊的,會無端造成請求頻寬的浪費(想象一下我們的cookie大小假設為10K,100個請求就是近1M的大小,高併發下以我們現行網路頻寬也是蠻大的一筆負擔了)。Cookie free效能優化方案的處理方式是CDN異域靜態資源伺服器部署我們的前端css、js、image資源。

以自己目前負責的香港跨境匯款為例

頁面路徑下的資源的請求:

CDN資源載入的請求:

通過對比CDN分開部署的資源請求並沒有帶上cookie資訊。

4、突破瀏覽器併發連線限制

瀏覽器針對domain,而非頁面page做併發連線限制的特性,domain hash的技術優化方案的處理方式是將資源劃分域分開部署,但因為過多的域劃分會增加多餘的DNS開銷,這裡通行的數量是3個以內。目前我們的港菲匯款業務只有兩個域名分開部署,一個主站,一個CDN,我個人建議可以將CDN中的圖片資源再單獨再分一個域名部署會更好些,為什麼單獨把圖片抽出來,後面會講到。

5、利用GPU硬體加速瀏覽器渲染

針對一些介面渲染過程比較耗時的情況下,可以利用CSS3屬性開啟GPU來加速渲染我們的DOM,開啟很簡單一般我是用-webkit-transform:translateZ(0)假3D屬性來喚起系統GPU加速渲染功能,關於為什麼會這樣,我這裡做個簡單的解釋:

對於我們的瀏覽器而言,拿到我們的html文字串開始按順序解析成DOM樹,並與同步解析出來的CSS匹配生成渲染樹(跟DOM樹的節點不是一一對應,比如display:none的節點就不會插入渲染樹)

圖片來源 segmentfault.com/a/119000000…

瀏覽器將渲染樹的節點用一個圖層表示,這樣層層疊加在一起生成layout,有點像ps的圖層疊加的概念(可以通過火狐瀏覽器開發者工具3維展示更直觀),一般情況下對節點的任何涉及尺寸的改變都會引起layout的重排重繪(重排和重繪是造成瀏覽器渲染的最大效能損耗的因素),但有種開小灶的情況Composite Layers(複合圖層)直接交給我們GPU中單獨的合成器程式處理,自身變化不會引起其他層的位置變化,不會引起重排重繪。tranform 3d屬性是可以悄悄的告訴我們的瀏覽器把元素解析作為複合圖層交給單獨程式去處理的。

注:這裡有個原則,不能濫用我們的加速,因為過多開啟硬體加速會消耗更多的使用者記憶體空間,也會比較耗電,一般針對css3動畫建議開啟

二、Http維度

1、減少http請求數量

a、通行解決方案

  • css、script合併:gulp、webpack都能夠很簡單的通過任務指令碼的方式去自動化解決,目前我們團隊是用我們自研的前端構建工具配合我們的Dust庫做的釋出前的資源打包任務,核心就是用的gulp。
  • css sprites雪碧圖:將網站常用的一些小圖片整合到一張大圖上來,樣式裡面通過background-position二維座標定位找到自己的圖片。這裡有個原則,一般是將網站複用率較高的,不太容易變動的圖示和圖片,比如按鈕、平鋪背景小圖片等。
  • font-icon字型圖示:字型圖示庫的使用,是一個非常有創新的方式,因為是向量的,解決點陣圖畫素放大變虛的問題,體驗很好,相比同樣向量的SVG來說使用更簡單,一個css的font-family就可以像平時設定字型一樣使用,淘寶是國內這方面的先行者,有自己的一套很開放的向量圖示庫平臺。淘寶自身的許多小圖示都是用的字型圖示來展示的。

  • 圖片base64編碼傳輸:圖片base64編碼後,可以讓瀏覽器減少自身的一次http請求,但因為自身的一些缺陷,不能濫用(即使一個很小的圖片編碼後都會有一大串字元,增加了我們CSS體積,效能不降反升),我的建議是針對那些全站通用或者體積很小不好整合到雪碧圖裡面的圖示進行編碼,當然還有很多不同的場景大家自己權衡。

  • 圖片延時載入:主要是為了減少首屏一次性圖片的載入量。具體做法是給圖片或者標籤設定一個私有行內屬性data-image(當然可以自己隨便定義)存放目標圖片地址資訊,監聽瀏覽器的滾動事件,標籤到了瀏覽器可視區域就將圖片地址放入圖片的src屬性中或者作為標籤的樣式的背景圖片中展示。淘寶首頁的做法是用一個div來做延時圖片載入,通過背景圖片來展示最後的圖片。

圖片展示前:

圖片展示後:

b、快取機制

  • 協議快取方案:利用http快取協議頭cache-control做304快取,或者更精確的ETAG設定依據資源的修改時間來設定快取方案。但目前更有效或者極端的做法是利用max-expire-time,設定資源的最大快取時間假設為1年的長快取,更新採用非覆蓋式更新的方式是目前大公司通行的做法。這樣每次資源請求的時候都是隻從客戶端快取讀取(status:200,size:from cache),而不是還要跑一次http請求到伺服器端拿到304狀態。還是以一張淘寶首頁圖片長快取的截圖為例:

  • appCache應用快取方案:離線應用快取是h5提供一個比較有效的離線應用方案,利用navigator.online 、window.applicationCache物件、伺服器.appcache(以前是.manifest)配置檔案保證在離線下的移動web應用照常能用,如果要做資料的離線還要加上window.localStorage做離線資料的儲存。這裡簡單說一下接入離線應用需要的幾個步驟:

1、給需要做離線快取的頁面html標籤設定manifest屬性,指定快取的配置檔案 cache.appcahe(可以設定任何副檔名,只要在伺服器端配置mime-type為text/cache-manifest就行)。

2、建立上一步指定的cache.appcache配置檔案,按以下截圖說明來配置資源

3、在伺服器端配置配置檔案的副檔名對映的mime-type為text/cache-manifest

appcache離線方案詬病太多,目前接入的不多,有種慢慢變棄兒的趨勢,這裡提出來讓大家權衡

  • PWA(Progressive Web Apps)方案:谷歌提出的一套全新的離線web方案,利用manifest.json配置檔案、window.serviceWorker物件來實現類原生app體驗的離線應用方案,可以說是瀏覽器應用快取的一個脫胎升級方案(鑑於文章篇幅,這裡不做介紹了)。

2、減輕http資料請求大小

a、通行解決方案

  • css、script、圖片壓縮:這些可以gulp或者webpack自動化指令碼里面定義指令碼任務來完成。

  • 伺服器開啟gzip壓縮:一般現在伺服器都有開啟Gzip壓縮,壓縮率通常都是30%以上,效果還是不錯的。

原圖:

Gzip壓縮後:

  • 圖片伺服器動態響應方案:這個方案對應上面宿主環境維度domain hash單獨出來一個獨立域名部署圖片資源的方案介紹。圖片資源是網站請求資源中一個非常大頭的開銷,以前大家可以在靜態資源伺服器中建個image目錄存放就完事,隨著網站服務發展,圖片不僅面臨多樣化、高併發帶來的壓力,在移動端wap站點中更是要針對不同的解析度螢幕下圖片尺寸動態適配的場景以為了節省頻寬的需求。圖片伺服器的單獨架構有一定的複雜度(如果考慮到高併發下的容災、快取機制的話不亞於一個大型web網站叢集的搭建,這裡有篇文章推薦大家閱讀),這裡只討論一下其中負責切圖服務部分的伺服器(簡稱切圖伺服器)功能,切圖伺服器對外提供一個restful的url呼叫,比如 www.xxx.com/xxx圖片路徑.../… 告訴切圖伺服器將“xxx圖片路徑”下的xxx圖片等比壓縮成130*120的圖片尺寸並返回,這塊服務可以使用我們前端比較熟悉的node建立,當然也可以用PHP來提供。

b、頁面切片預載入方案

效能優化靜態資源維度最後一塊內容就是針對頁面,如何儘早輸出頁面模組,減少留白時間是一個思考點。facebook應用的BigPipe方案是個很不錯的借鑑思想,還有淘寶也有首頁做了相應的切片方案,對頁面合理的分塊,在伺服器和客戶端建立某種對應機制,讓各個頁面塊並行的在伺服器端拼接完成並吐出來,目前我對這塊沒有太深的瞭解,這裡只是提出bigPipe的方案供大家參考。

三、TCP維度

TCP連線中的3次握手、慢啟動的一些特性註定了連線通道的利用效率成為制約效能的一個很大的因素。因為http是基於TCP的應用協議,TCP層維度考慮還得從http幾個版本的發展歷史來看:

  • http1.0時期:tcp連線是基於一種單通道順序等待請求響應方式(客戶端每發一個請求都要重新建立連線),特定歷史背景下產生的,低效率很難跟上時代發展,99年在1.0基礎上修訂出1.1版本,並沿用至今。
  • http1.1時期:在請求頭資訊加入keep Alive保持連線的一定活性(當然也加入了100 Status節約頻寬、cache特性等),允許在一個連線通道生命期內重複傳送不同的應用請求,一定程度上減輕了連線資源利用效率問題,但當使用者瀏覽網頁時間大於連線活性週期再次請求的時候仍然要重新建立這些請求,在大型科技公司對高併發高可用性下資源高效利用的背景下,1.1版本還是難滿足大公司對高效能條件下網路資源的高效率利用的要求。

谷歌(叒是谷歌,牛逼)率先在09年基於TCP開發出全新SPDY應用協議,解決了多路複用請求優化、伺服器推送的痛點問題,也為後面http2.0的推出奠定了基礎。
我們可以做的優化:減少一些不必要的請求(掃除404死連線、304請求用我們的長快取機制)去優化,儘量減少一些不必要的連線請求數。

四、程式碼實現

鑑於js語言本身的靈活性,以及每個人的開發習慣,很難有很好的一個方式去驗證開發者的程式碼實現的效率(目前更多的是用打點測速的方式去監控程式碼的執行時間),更多的是一種建議,大家有更好的建議可以提出來分享。

  • 單執行緒限制:利用非同步回撥&多執行緒API突破js語言單執行緒帶來的記憶體開銷利用不充分的問題,現有可以利用的一些非同步方式的回撥都可以嘗試利用比如settimeout,setinterval,requestAnimationframe(推薦用它),多執行緒的API方式有WebWork。這裡推薦日本的一個開發者開發的一個多執行緒前端庫Concurrent.Thread.js(它是作者用setTimeout和setInterval來模擬的多執行緒,可以自行網上搜尋瞭解)。

  • 重點優化程式碼中的迴圈結構體:就程式碼本身而言影響執行效率的大部分是迴圈體結構和演算法設計不合理導致的時間複雜度和空間複雜度的增加。這裡放兩段實際專案中的程式碼截圖來對比:

例項功能需要:實現輸入框每4個字元一個空格隔開的效果

低效實現方式,用for迴圈:

改進後的方式:

  • 合理使用設計模式優化程式碼結構:設計模式的合理利用(不能濫用)可以起到記憶體優化提高執行效率效果,比如單例和簡單工廠在建立xhr請求物件中的利用:建立一個簡單工廠向外面提供xhr物件,工廠內部用閉包開闢一個陣列佇列單例用來存放xhr物件,當呼叫者需要xhr物件時工廠就從佇列裡取出readyState狀態為空閒(0/4)的xhr物件否則重新建立並放入佇列中。在有大量ajax物件請求的應用下可以最大限度節約建立xhr消耗的記憶體開銷,這裡用個簡圖來描述一下思路:

  • 其他一些優化建議:比如減少js頻繁操作dom節點的次數(這個本來想放在宿主環境維度)減少瀏覽器的重排重繪;比如針對dom標籤物件(主要是針對ie下dom物件的引用會被GC回收遺漏的問題)、閉包內部的引用型別的變數用完過後記得要及時釋放,避免造成記憶體洩漏。

五、產品互動邏輯

效能優化一般都是從技術角度去入手,但我們的目標之一“讓使用者簡單易用”也是效能優化的一環。當技術效能缺陷難於避免的時候,作為前端互動實現的執行者,更應該配合產品和互動設計師提出一個我們認為更好的互動邏輯體驗方案去讓我們的資料載入不那麼讓使用者有等待的感知,讓我們的提示更加的人性舒服。(互動設計師更加專業,我這裡不敢班門弄斧)

Web3.0時代的前端展望:

文章最後對Web3.0時代做個自己的猜想,web3.0目前在業內還沒有很明確定義,大家可以大膽猜想前端行業未來形態。我在這先YY一下,人工智慧、大資料廣泛應用應該會成為推動前端進入3.0的時代的最好契機,以此引發的前端新的革命:

  • 瀏覽器成為一個系統生態(至於哪個瀏覽器現在不好說,現在谷歌瀏覽器PWA方案提供給前端類app開發方案就有這個趨勢,以後都不需要裝系統了)。前端不再是資料的搬運工,在web領域很有可能除了底層維護資料庫的工程師們繼續深耕外(主要切入雲端計算領域),其它的都可以轉到前端做瀏覽器系統生態(哎媽呀,有種翻身農奴做主人的感覺O(∩_∩)O~~)。
  • 傳統的html語義性的超文字標記語言已經很難承載更多的資訊化資料,html5繼續深度發展,不再只是瀏覽器專用的標記語言,可能會成為跨平臺標準的資訊表示層,資訊表示多樣化前所未有,可能到時候不叫html了。
  • http2.0成為一個廣泛試用的標準,並進一步深化,安全校驗層做到類似區塊鏈去中心化的思路,做到極致安全(https估計可以下崗了)。
  • js成為跨平臺公認的標準實現語言(目前前端跨平臺基礎形態已經有了),隨著Es6的廣泛推廣和深度改進,可能就不會像現在這麼靈活,更加像一個合格的標準“高階”指令碼語言。
  • (以上猜想純屬個人理解,沒有權威認證,大家可以暢所欲言)

結語:剛來鵝廠不久,作為前端攻城獅進入到國內頂尖網際網路公司感到驕傲,效能優化是一個永恆的話題,每個階段都有聊不完的效能優化的話題,我將我這些年的一些不成文的理解整理了一下,希望對大家有點幫助。第一次發文章,有錯誤的地方請大家指點一二。

相關閱讀

億萬級訪問量下的前端同構直出實踐

騰訊的專項測試之道

從 10 Gb 到 40 Gb,從百萬級到千萬級轉發,打造高效能 TGW

此文已由作者授權騰訊雲技術社群釋出,轉載請註明文章出處

原文連結:https://cloud.tencent.com/community/article/879614


相關文章