天天看點

鳥瞰前端 , 再論性能優化

從事前端有 6 年+的時間了,我現在将自己這些年的一個心得體會來個系統性的梳理寫成一篇關于性能優化的主題文章,希望對大家有點幫助,也歡迎大家提出各種意見和建議。

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

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

作者:劉勇剛 

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

鳥瞰前端 , 再論性能優化

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

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

“從使用者輸入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的節點就不會插入渲染樹)

鳥瞰前端 , 再論性能優化

圖檔來源 https://segmentfault.com/a/1190000008650975

浏覽器将渲染樹的節點用一個圖層表示,這樣層層疊加在一起生成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資料請求大小

  • css、script、圖檔壓縮:這些可以gulp或者webpack自動化腳本裡面定義腳本任務來完成。
  • 伺服器開啟gzip壓縮:一般現在伺服器都有開啟Gzip壓縮,壓縮率通常都是30%以上,效果還是不錯的。

原圖:

鳥瞰前端 , 再論性能優化

Gzip壓縮後:

鳥瞰前端 , 再論性能優化
  • 圖檔伺服器動态響應方案:這個方案對應上面宿主環境次元domain hash單獨出來一個獨立域名部署圖檔資源的方案介紹。圖檔資源是網站請求資源中一個非常大頭的開銷,以前大家可以在靜态資源伺服器中建個image目錄存放就完事,随着網站服務發展,圖檔不僅面臨多樣化、高并發帶來的壓力,在移動端wap站點中更是要針對不同的分辨率螢幕下圖檔尺寸動态适配的場景以為了節省帶寬的需求。圖檔伺服器的單獨架構有一定的複雜度(如果考慮到高并發下的容災、緩存機制的話不亞于一個大型web網站叢集的搭建,這裡有篇文章推薦大家閱讀),這裡隻讨論一下其中負責切圖服務部分的伺服器(簡稱切圖伺服器)功能,切圖伺服器對外提供一個restful的url調用,比如http://www.xxx.com/xxx圖檔路徑.../xxx圖檔名/130_120 告訴切圖伺服器将“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

海量技術實踐經驗,盡在雲加社群!

https://cloud.tencent.com/developer