瀏覽器的載入與頁面效能優化
http://www.baiduux.com/blog/2011/02/15/browser-loading/
本文將探討瀏覽器渲染的loading過程,主要有2個目的:
- 瞭解瀏覽器在loading過程中的實現細節,具體都做了什麼
- 研究如何根據瀏覽器的實現原理進行優化,提升頁面響應速度
由於loading和parsing是相互交織、錯綜複雜的,這裡面有大量的知識點,為了避免過於發散本文將不會對每個細節都深入研究,而是將重點放在開發中容易控制的部分(Web前端和Web Server),同時由於瀏覽器種類繁多且不同版本間差距很大,本文將側重一些較新的瀏覽器特性
現有知識
提升頁面效能方面已經有很多前人的優秀經驗了,如Best Practices for Speeding Up Your Web Site和Web Performance Best Practices
本文主要專注其中載入部分的優化,總結起來主要有以下幾點:
- 頻寬
- 使用CDN
- 壓縮js、css,圖片優化
- HTTP優化
- 減少轉向
- 減少請求數
- 快取
- 儘早Flush
- 使用gzip
- 減少cookie
- 使用GET
- DNS優化
- 減少域名解析時間
- 增多域名提高併發
- JavaScript
- 放頁面底部
- defer/async
- CSS
- 放頁面頭部
- 避免@import
- 其它
- 預載入
接下來就從瀏覽器各個部分的實現來梳理效能優化方法
network
首先是網路層部分,這方面的實現大部分是通過呼叫作業系統或gui框架提供的api
DNS
為了應對DNS查詢的延遲問題,一些新的瀏覽器會快取或預解析DNS,如當Chrome訪問google頁面的搜尋結果時,它會取出連結中的域名進行預解析
當然,Chrome並不是每次都將頁面中的所有連結的域名都拿來預解析,為了既提升使用者體驗又不會對DNS造成太大負擔,Chrome做了很多細節的優化,如通過學習使用者之前的行為來進行判斷
Chrome在啟動時還會預先解析使用者常去的網站,具體可以參考DNS Prefetching,當前Chrome中的DNS快取情況可以通過net-internals頁面來察看
為了幫助瀏覽器更好地進行DNS的預解析,可以在html中加上以下這句標籤來提示瀏覽器
<link rel="dns-prefetch" href="//HOSTNAME.com">
除此之外還可以使用HTTP header中的X-DNS-Prefetch-Control來控制瀏覽器是否進行預解析,它有on和off兩個值,更詳細的資訊請參考Controlling DNS prefetching
CDN
本文不打算詳細討論這個話題,感興趣的讀者可以閱讀Content delivery network
在效能方面與此相關的一個問題是使用者可能使用自定義的DNS,如OpenDNS或Google的8.8.8.8,需要注意對這種情況進行處理
link prefetch
由於Web頁面載入是同步模型,這意味著瀏覽器在執行js操作時需要將後續html的載入和解析暫停,因為js中有可能會呼叫document.write來改變dom節點,很多瀏覽器除了html之外還會將css的載入暫停,因為js可能會獲取dom節點的樣式資訊,這個暫停會導致頁面展現速度變慢,為了應對這個問題,Mozilla等瀏覽器會在執行js的同時簡單解析後面的html,提取出連結地址提前下載,注意這裡僅是先下載內容,並不會開始解析和執行
這一行為還可以通過在頁面中加入以下標籤來提示瀏覽器
<link rel="prefetch" href="http://">
但這種寫法目前並沒有成為正式的標準,也只有Mozilla真正實現了該功能,可以看看Link prefetching FAQ
WebKit也在嘗試該功能,具體實現是在HTMLLinkElement的process成員函式中,它會呼叫ResourceHandle::prepareForURL()函式,目前從實現看它是僅僅用做DNS預解析的,和Mozilla對這個屬性的處理不一致
對於不在當前頁面中的連結,如果需要預下載後續內容可以用js來實現,請參考這篇文章Preload CSS/JavaScript without execution
預下載後續內容還能做很多細緻的優化,如在Velocity China
2010中,來自騰訊的黃希彤介紹了騰訊產品中使用的交叉預下載方案,利用空閒時間段的流量來預載入,這樣即提升了使用者訪問後續頁面的速度,又不會影響到高峰期的流量,值得借鑑
預渲染
預渲染比預下載更進一步,不僅僅下載頁面,而且還會預先將它渲染出來,目前在Chrome(9.0.597.0)中有實現,不過需要在about:flags中將’Web Page Prerendering’開啟
不得不說Chrome的效能優化做得很細緻,各方面都考慮到了,也難怪Chrome的速度很快
http
在網路層之上我們主要關注的是HTTP協議,這裡將主要討論1.1版本,如果需要了解1.0和1.1的區別請參考Key Differences between HTTP/1.0 and HTTP/1.1
header
首先來看http中的header部分
header大小
header的大小一般會有500 多位元組,cookie內容較多的情況下甚至可以達到1k以上,而目前一般寬頻都是上傳速度慢過下載速度,所以如果小檔案多時,甚至會出現頁面效能瓶頸出在使用者上傳速度上的情況,所以縮小header體積是很有必要的,尤其是對不需要cookie的靜態檔案上,最好將這些靜態檔案放到另一個域名上
將靜態檔案放到另一個域名上會出現的現象是,一旦靜態檔案的域名出現問題就會對頁面載入造成嚴重影響,尤其是放到頂部的js,如果它的載入受阻會導致頁面展現長時間空白,所以對於流量大且內容簡單的首頁,最好使用內嵌的js和css
header的擴充套件屬性
header中有些擴充套件屬性可以用來保護站點,瞭解它們是有益處的
- X-Frame-Options
- 這個屬性可以避免網站被使用frame、iframe的方式嵌入,解決使用js判斷會被var location;破解的問題,IE8、Firefox3.6、Chrome4以上的版本都支援
- X-XSS-Protection
- 這是IE8引入的擴充套件header,在預設情況下IE8會自動攔截明顯的XSS攻擊,如query中寫script標籤並在返回的內容中包含這項標籤,如果需要禁止可以將它的值設為0,因為這個XSS過濾有可能導致問題,如IE8 XSS Filter Bug
- X-Requested-With
- 用來標識Ajax請求,大部分js框架都會加入這個header
- X-Content-Type-Options
- 如果是html內容的檔案,即使用Content-Type: text/plain;的header,IE仍然會識別成html來顯示,為了避免它所帶來的安全隱患,在IE8中可以通過在header中設定X-Content-Type-Options: nosniff來關閉它的自動識別功能
使用get請求來提高效能
首先效能因素不應該是考慮使用get還是post的主要原因,首先關注的應該是否符合HTTP中標準中的約定,get應該用做資料的獲取而不是提交
之所以用get效能更好的原因是有測試表明,即使資料很小,大部分瀏覽器(除了Firefox)在使用post時也會傳送兩個TCP的packet,所以效能上會有損失
連線數
在HTTP/1.1協議下,單個域名的最大連線數在IE6中是2個,而在其它瀏覽器中一般4-8個,而整體最大連結數在30左右
而在HTTP/1.0協議下,IE6、7單個域名的最大連結數可以達到4個,在Even Faster Web Sites一書中的11章還推薦了對靜態檔案服務使用HTTP/1.0協議來提高IE6、7瀏覽器的速度
瀏覽器連結數的詳細資訊可以在Browserscope上查到
使用多個域名可以提高併發,但前提是每個域名速度都是同樣很快的,否則就會出現某個域名很慢會成為效能瓶頸的問題
cache
主流瀏覽器都遵循http規範中的Caching in HTTP來實現的
從HTTP cache的角度來看,瀏覽器的請求分為2種型別:conditional requests 和 unconditional requests
unconditional請求是當本地沒有快取或強制重新整理時發的請求,web server返回200的heder,並將內容傳送給瀏覽器
而conditional則是當本地有快取時的請求,它有兩種:
- 使用了Expires或Cache-Control,如果本地版本沒有過期,瀏覽器不會發出請求
- 如果過期了且使用了ETag或Last-Modified,瀏覽器會發起conditional請求,附上If-Modified-Since或If-None-Match的header,web server根據它來判斷檔案是否過期,如果沒有過期就返回304的header(不返回內容),瀏覽器見到304後會直接使用本地快取中的檔案
以下是IE傳送conditional requests的條件,從MSDN上抄來
- The cached item is no longer fresh according to Cache-Control or Expires
- The cached item was delivered with a VARY header
- The containing page was navigated to via META REFRESH
- JavaScript in the page called reload on the location object, passing TRUE
- The request was for a cross-host HTTPS resource on browser startup
- The user refreshed the page
簡單的來說,點選重新整理按鈕或按下F5時會發出conditional請求,而按下ctrl的同時點選重新整理按鈕或按下F5時會發出unconditional請求
需要進一步學習請閱讀:
前進後退的處理
瀏覽器會盡可能地優化前進後退,使得在前進後退時不需要重新渲染頁面,就好像將當前頁面先“暫停”了,後退時再重新執行這個“暫停”的頁面
不過並不是所有頁面都能“暫停”的,如當頁面中有函式監聽unload事件時,所以如果頁面中的連結是原視窗開啟的,對於unload事件的監聽會影響頁面在前進後時的效能
在新版的WebKit裡,在事件的物件中新增了一個persisted屬性,可以用它來區分首次載入和通過後退鍵載入這兩種不同的情況,而在Firefox中可以使用pageshow和pagehide這兩個事件
unload事件在瀏覽器的實現中有很多不確定性因素,所以不應該用它來記錄重要的事情,而是應該通過定期更新cookie或定期儲存副本(如使用者備份編輯文章到草稿中)等方式來解決問題
具體細節可以參考WebKit上的這2篇文章:
cookie
瀏覽器中對cookie的支援一般是網路層庫來實現的,瀏覽器不需要關心,如IE使用的是WinINET
需要注意IE對cookie的支援是基於pre-RFC Netscape draft spec for cookies的,和標準有些不同,在設定cookie時會出現轉義不全導致的問題,如在ie和webkit中會忽略“=”,不過大部分web開發程式(如php語言)都會處理好,自行編寫http互動時則需要注意
p3p問題
在IE中預設情況下iframe中的頁面如果域名和當前頁面不同,iframe中的頁面是不會收到cookie的,這時需要通過設定p3p來解決,具體可以察看微軟官方的文件,加上如下header即可
P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
這對於用iframe嵌入到其它網站中的第三方應用很重要
編碼識別
頁面的編碼可以在http header或meta標籤中指明,對於沒有指明編碼的頁面,瀏覽器會根據是否設定了auto detect來進行編碼識別(如在chrome中的View-Encoding選單)
關於編碼識別,Mozilla開源了其中的Mozilla Charset Detectors模組,感興趣的可以對其進行學習
建議在http
header中指定編碼,如果是在meta中指定,瀏覽器在得到html頁面後會首先讀取一部分內容,進行簡單的meta標籤解析來獲得頁面編碼,如WebKit程式碼中的HTMLMetaCharsetParser.cpp,可以看出它的實現是查詢charset屬性的值,除了WebKit以外的其它瀏覽器也是類似的做法,這就是為何HTML5中直接使用如下的寫法瀏覽器都支援
<meta charset="utf-8">
需要注意不設定編碼會導致不可預測的問題,應儘可能做到明確指定
chunked
瀏覽器在載入html時,只要網路層返回一部分資料後就會開始解析,並下載其中的js、圖片,而不需要等到所有html都下載完成才開始,這就意味著如果可以分段將資料傳送給瀏覽器,就能提高頁面的效能,這就是chunked的作用,具體協議細節請參考Chunked Transfer Coding
在具體實現上,php中可以通過flush函式來實現,不過其中有不少需要注意的問題,如php的配置、web server、某些IE版本的問題等,具體請參考php文件及評論
注意這種方式只適用於html頁面,對於xml型別的頁面,由於xml的嚴格語法要求,瀏覽器只能等到xml全部下載完成後才會開始解析,這就意味著同等情況下,xml型別的頁面展現速度必然比html慢,所以不推薦使用xml
即使不使用這種http傳輸方式,瀏覽器中html載入也是邊下載邊解析的,而不需等待所有html內容都下載完才開始,所以實際上chunked主要節省的是等待伺服器響應的時間,因為這樣可以做到伺服器計算完一部分頁面內容後就立刻返回,而不是等到所有頁面都計算都完成才返回,將操作並行
另外Facebook所使用的BigPipe實際上是在應用層將頁面分為了多個部分,從而做到了服務端和瀏覽器計算的並行
keepalive
keepalive使得在完成一個請求後可以不關閉socket連線,後續可以重複使用該連線傳送請求,在HTTP/1.0和HTTP/1.1中都有支援,在HTTP/1.1中預設是開啟的
keepalive在瀏覽器中都會有超時時間,避免長期和伺服器保持連線,如IE是60秒
另外需要注意的是如果使用阻塞IO(如apache),開啟keepalive保持連線會很消耗資源,可以考慮使用nginx、lighttpd等其它web server,具體請參考相關文件,這裡就不展開描述
pipelining
pipelining是HTTP/1.1協議中的一個技術,能讓多個HTTP請求同時通過一個socket傳輸,注意它和keepalive的區別,keepalive能在一個socket中傳輸多個HTTP,但這些HTTP請求都是序列的,而pipelining則是並行的
可惜目前絕大部分瀏覽器在預設情況下都不支援,已知目前只有opera是預設支援的,加上很多網路代理對其支援不好導致容易出現各種問題,所以並沒有廣泛應用
SPDY
SPDY是google提出的對HTTP協議的改進,主要是目的是提高載入速度,主要有幾點:
- Mutiplexed streams
- 可以在一個TCP中傳輸各種資料,減少連結的耗時
- Request prioritization
- 請求分級,便於傳送方定義哪些請求是重要的
- HTTP header compression
- header壓縮,減少資料量
frame
從實現上看,frame類(包括iframe和frameset)的標籤是最耗時的,而且會導致多一個請求,所以最好減少frame數量
resticted
如果要嵌入不信任的網站,可以使用這個屬性值來禁止頁面中js、ActiveX的執行,可以參考msdn的文件
<iframe security="restricted" src=""></iframe>
javascript
載入
對於html的script標籤,如果是外鏈的情況,如:
<script src="a.js"></script>
瀏覽器對它的處理主要有2部分:下載和執行
下載在有些瀏覽器中是並行的,有些瀏覽器中是序列的,如IE8、Firefox3、Chrome2都是序列下載的
執行在所有瀏覽器中預設都是阻塞的,當js在執行時不會進行html解析等其它操作,所以頁面頂部的js不宜過大,因為那樣將導致頁面長時間空白,對於這些外鏈js,有2個屬性可以減少它們對頁面載入的影響,分別是:
- async
- 標識js是否非同步執行,當有這個屬性時則不阻塞當前頁面的載入,並在js下載完後立刻執行
- 不能保證多個script標籤的執行順序
- defer
- 標示js是否延遲執行,當有這個屬性時js的執行會推遲到頁面解析完成之後
- 可以保證多個script標籤的執行順序
下圖來自Asynchronous and deferred JavaScript execution explained,清晰地解釋了普通情況和這2種情況下的區別
需要注意的是這兩個屬性目前對於內嵌的js是無效的
而對於dom中建立的script標籤在瀏覽器中則是非同步的,如下所示:
var script = document.createElement('script'); script.src = 'a.js'; document.getElementsByTagName('head')[0].appendChild(script);
為了解決js阻塞頁面的問題,可以利用瀏覽器不認識的屬性來先下載js後再執行,如ControlJS就是這樣做的,它能提高頁面的相應速度,不過需要注意處理在js未載入完時的顯示效果
document.write
document.write是不推薦的api,對於標示有async或defer屬性的script標籤,使用它會導致不可預料的結果,除此之外還有以下場景是不應該使用它的:
- 使用document.createElement建立的script
- 事件觸發的函式中,如onclick
- setTimeout/setInterval
簡單來說,document.write只適合用在外鏈的script標籤中,它最常見的場景是在廣告中,由於廣告可能包含大量html,這時需要注意標籤的閉合,如果寫入的內容很多,為了避免受到頁面的影響,可以使用類似Google AdSense的方式,通過建立iframe來放置廣告,這樣做還能減少廣告中的js執行對當前頁面效能的影響
另外,可以使用ADsafe等方案來保證嵌入第三方廣告的安全,請參考如何安全地嵌入第三方js – FBML/caja/sandbox/ADsafe簡介
script標籤放底部
將script標籤放底部可以提高頁面展現給使用者的速度,然而很多時候事情並沒那麼簡單,如頁面中的有些功能是依賴js的,所以更多的還需要根據實際需求進行調整
- 嘗試用Doloto分析出哪些JS和初始展現是無關的,將那些不必要的js延遲載入
- 手工進行分離,如可以先顯示出按鈕,但狀態是不可點,等JS載入完成後再改成可點的
傳輸
js壓縮可以使用YUI Compressor或Closure Compiler
gwt中的js壓縮還針對gzip進行了優化,進一步減小傳輸的體積,具體請閱讀On Reducing the Size of Compressed Javascript
css
比起js放底部,css放頁面頂部就比較容易做到
@import
使用@import在IE下會由於css載入延後而導致頁面展現比使用link標籤慢,不過目前幾乎沒有人使用@import,所以問題不大,具體細節請參考don’t use @import
selector的優化
瀏覽器在構建DOM樹的過程中會同時構建Render樹,我們可以簡單的認為瀏覽器在遇到每一個DOM節點時,都會遍歷所有selector來判斷這個節點會被哪些selector影響到
不過實際上瀏覽器一般是從右至左來判斷selector是否命中的,對於ID、Class、Tag、Universal和Page的規則是通過hashmap的方式來查詢的,它們並不會遍歷所有selector,所以selector越精確越好,google page-speed中的一篇文件Use efficient CSS selectors詳細說明了如何優化selector的寫法
另一個比較好的方法是從架構層面進行優化,將頁面不同部分的模組和樣式繫結,通過不同組合的方式來生成頁面,避免後續頁面頂部的css只增不減,越來越複雜和混亂的問題,可以參考Facebook的靜態檔案管理
工具
以下整理一些效能優化相關的工具及方法
Browserscope
之前提到的http://www.browserscope.org收集了各種瀏覽器引數的對比,如最大連結數等資訊,方便參考
Navigation Timing
Navigation Timing是還在草案中的獲取頁面效能資料api,能方便頁面進行效能優化的分析
傳統的頁面分析方法是通過javascript的時間來計算,無法獲取頁面在網路及渲染上所花的時間,使用Navigation Timing就能很好地解決這個問題,具體它能取到哪些資料可以通過下圖瞭解(來自w3c)
目前這個api較新,目前只在一些比較新的瀏覽器上有支援,如Chrome、IE9,但也佔用一定的市場份額了,可以現在就用起來
boomerang
yahoo開源的一個頁面效能檢測工具,它的原理是通過監聽頁面的onbeforeunload事件,然後設定一個cookie,並在另一個頁面中設定onload事件,如果cookie中有設定且和頁面的refer保持一致,則通過這兩個事件的事件來衡量當前頁面的載入時間
另外就是通過靜態圖片來衡量頻寬和網路延遲,具體可以看boomerang
檢測工具
reference
- Browser Performance Wishlist
- HTML5
- Testing Page Load Speed
- Technically speaking, what makes Google Chrome fast?
- Optimizing Page Load Time
- An Engineer’s Guide to Bandwidth
- An Engineer’s Guide to DNS
- EricLaw’s IEInternals
- Internet Explorer Platform for Privacy Preferences (P3P) Standards Support Document
- COMET Streaming in Internet Explorer
- Internet Explorer Cookie Internals (FAQ)
- Fiddler PowerToy – Part 2: HTTP Performance
- Frontend SPOF
- XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?
- WebKit Page Cache I – The Basics
- WebKit Page Cache II – The unload Event
相關文章
- 瀏覽器層合成與頁面效能優化瀏覽器優化
- 瀏覽器頁面資源載入過程與優化瀏覽器優化
- 瀏覽器頁面載入過程瀏覽器
- 頁面載入效能之優化LCP優化
- 瀏覽器渲染流水線解析與網頁動畫效能優化瀏覽器網頁動畫優化
- iOS效能優化之頁面載入速率iOS優化
- 瀏覽器效能優化-渲染效能瀏覽器優化
- 瀏覽器渲染過程與效能優化瀏覽器優化
- 【效能優化實踐】優化打包策略提升頁面載入速度優化
- 無線效能優化:頁面可見時間與非同步載入優化非同步
- 前端效能優化之桌面瀏覽器優化策略前端優化瀏覽器
- 前端效能優化——桌面瀏覽器前端優化策略前端優化瀏覽器
- 2013年瀏覽器大戰:黑莓5.0瀏覽器頁面載入速度最快–資訊圖瀏覽器
- ArkWeb頁面載入與瀏覽記錄導航 - 基礎操作Web
- 前端效能優化 —— 移動端瀏覽器優化策略前端優化瀏覽器
- 前端效能優化(一)——瀏覽器工作原理前端優化瀏覽器
- 前端效能優化 之 瀏覽器快取前端優化瀏覽器快取
- 現代瀏覽器效能優化-JS篇瀏覽器優化JS
- 前端效能優化:細說瀏覽器渲染的重排與重繪前端優化瀏覽器
- 從頁面載入到資料請求,前端頁面效能優化實踐分享前端優化
- 【高效能JS】重繪、重排與瀏覽器優化方法JS瀏覽器優化
- 前端效能優化之移動端瀏覽器優化策略前端優化瀏覽器
- iOS 頁面效能優化iOS優化
- 讓我們再聊聊瀏覽器資源載入優化瀏覽器優化
- 10種優化頁面載入速度的方法優化
- 瀏覽器頁面渲染機制瀏覽器
- 瀏覽器彈出小頁面瀏覽器
- 從瀏覽器渲染原理談動畫效能優化瀏覽器動畫優化
- 瀏覽器工作原理及web 效能優化(上)瀏覽器Web優化
- 移動網站效能優化:網頁載入技術概覽網站優化網頁
- 瀏覽器前端優化瀏覽器前端優化
- 前端基於瀏覽器儲存的AJAX效能優化前端瀏覽器優化
- 瀏覽器重繪(repaint)重排(reflow)與優化[瀏覽器機制]瀏覽器AI優化
- 前端效能優化(二)——瀏覽器快取機制前端優化瀏覽器快取
- VS Code在瀏覽器預覽HTML頁面瀏覽器HTML
- 谷歌瀏覽器頁面放大縮小谷歌瀏覽器
- web 頁面內容優化管理與效能技巧Web優化
- 瀏覽器輸入URL到 請求全過程以及相應的效能優化瀏覽器優化