本文將探討瀏覽器渲染的 loading 過程,主要有 2 個目的:javascript
因爲 loading 和 parsing 是相互交織、錯綜複雜的,這裏面有大量的知識點,爲了不過於發散本文將不會對每一個細節都深刻研究,而是將重點放在開發中容易控制的部分(Web 前端和 Web Server),同時因爲瀏覽器種類繁多且不一樣版本間差距很大,本文將側重一些較新的瀏覽器特性php
提高頁面性能方面已經有不少前人的優秀經驗了,如 Best Practices for Speeding Up Your Web Site 和 Web Performance Best Practicescss
本文主要專一其中加載部分的優化,總結起來主要有如下幾點:html
接下來就從瀏覽器各個部分的實現來梳理性能優化方法前端
首先是網絡層部分,這方面的實現大部分是經過調用操做系統或 gui 框架提供的 apihtml5
爲了應對 DNS 查詢的延遲問題,一些新的瀏覽器會緩存或預解析 DNS,如當 Chrome 訪問 google 頁面的搜索結果時,它會取出連接中的域名進行預解析java
固然,Chrome 並非每次都將頁面中的全部連接的域名都拿來預解析,爲了既提高用戶體驗又不會對 DNS 形成太大負擔,Chrome 作了不少細節的優化,如經過學習用戶以前的行爲來進行判斷nginx
Chrome 在啓動時還會預先解析用戶常去的網站,具體能夠參考 DNS Prefetching,當前 Chrome 中的 DNS 緩存狀況能夠經過 net-internals 頁面來察看git
爲了幫助瀏覽器更好地進行 DNS 的預解析,能夠在 html 中加上如下這句標籤來提示瀏覽器github
除此以外還可使用 HTTP header 中的 X-DNS-Prefetch-Control 來控制瀏覽器是否進行預解析,它有 on 和 off 兩個值,更詳細的信息請參考 Controlling DNS prefetching
本文不打算詳細討論這個話題,感興趣的讀者能夠閱讀 Content delivery network
在性能方面與此相關的一個問題是用戶可能使用自定義的 DNS,如 OpenDNS 或 Google 的 8.8.8.8,須要注意對這種狀況進行處理
因爲 Web 頁面加載是同步模型,這意味着瀏覽器在執行 js 操做時須要將後續 html 的加載和解析暫停,由於 js 中有可能會調用 document.write 來改變 dom 節點,不少瀏覽器除了 html 以外還會將 css 的加載暫停,由於 js 可能會獲取 dom 節點的樣式信息,這個暫停會致使頁面展示速度變慢,爲了應對這個問題,Mozilla 等瀏覽器會在執行 js 的同時簡單解析後面的 html,提取出連接地址提早下載,注意這裏僅是先下載內容,並不會開始解析和執行
這一行爲還能夠經過在頁面中加入如下標籤來提示瀏覽器
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 協議,這裏將主要討論 1.1 版本,若是須要了解 1.0 和 1.1 的區別請參考Key Differences between HTTP/1.0 and HTTP/1.1
首先來看 http 中的 header 部分
header 的大小通常會有 500 多字節,cookie 內容較多的狀況下甚至能夠達到 1k 以上,而目前通常寬帶都是上傳速度慢過下載速度,因此若是小文件多時,甚至會出現頁面性能瓶頸出在用戶上傳速度上的狀況,因此縮小 header 體積是頗有必要的,尤爲是對不須要 cookie 的靜態文件上,最好將這些靜態文件放到另外一個域名上
將靜態文件放到另外一個域名上會出現的現象是,一旦靜態文件的域名出現問題就會對頁面加載形成嚴重影響,尤爲是放到頂部的 js,若是它的加載受阻會致使頁面展示長時間空白,因此對於流量大且內容簡單的首頁,最好使用內嵌的 js 和 css
header 中有些擴展屬性能夠用來保護站點,瞭解它們是有益處的
首先性能因素不該該是考慮使用 get 仍是 post 的主要緣由,首先關注的應該是否符合 HTTP 中標準中的約定,get 應該用作數據的獲取而不是提交
之因此用 get 性能更好的緣由是有測試代表,即便數據很小,大部分瀏覽器(除了 Firefox)在使用 post 時也會發送兩個 TCP 的 packet,因此性能上會有損失
在 HTTP/1.1 協議下,單個域名的最大鏈接數在 IE6 中是 2 個,而在其它瀏覽器中通常 4-8 個,而總體最大連接數在 30 左右
而在 HTTP/1.0 協議下,IE六、7 單個域名的最大連接數能夠達到 4 個,在 Even Faster Web Sites 一書中的 11 章還推薦了對靜態文件服務使用 HTTP/1.0 協議來提升 IE六、7 瀏覽器的速度
瀏覽器連接數的詳細信息能夠在 Browserscope 上查到
使用多個域名能夠提升併發,但前提是每一個域名速度都是一樣很快的,不然就會出現某個域名很慢會成爲性能瓶頸的問題
主流瀏覽器都遵循 http 規範中的 Caching in HTTP 來實現的
從 HTTP cache 的角度來看,瀏覽器的請求分爲 2 種類型:conditional requests 和 unconditional requests
unconditional 請求是當本地沒有緩存或強制刷新時發的請求,web server 返回 200 的 heder,並將內容發送給瀏覽器
而 conditional 則是當本地有緩存時的請求,它有兩種:
如下是 IE 發送 conditional requests 的條件,從 MSDN 上抄來
簡單的來講,點擊刷新按鈕或按下 F5 時會發出 conditional 請求, 而按下 ctrl 的同時點擊刷新按鈕或按下 F5 時會發出 unconditional 請求
須要進一步學習請閱讀:
瀏覽器會盡量地優化前進後退,使得在前進後退時不須要從新渲染頁面,就好像將當前頁面先 「暫停」 了,後退時再從新運行這個 「暫停」 的頁面
不過並非全部頁面都能 「暫停」 的,如當頁面中有函數監聽 unload 事件時,因此若是頁面中的連接是原窗口打開的,對於 unload 事件的監聽會影響頁面在前進後時的性能
在新版的 WebKit 裏,在事件的對象中新增了一個 persisted 屬性,能夠用它來區分首次載入和經過後退鍵載入這兩種不一樣的狀況,而在 Firefox 中可使用 pageshow 和 pagehide 這兩個事件
unload 事件在瀏覽器的實現中有不少不肯定性因素,因此不該該用它來記錄重要的事情,而是應該經過按期更新 cookie 或按期保存副本(如用戶備份編輯文章到草稿中)等方式來解決問題
具體細節能夠參考 WebKit 上的這 2 篇文章:
瀏覽器中對 cookie 的支持通常是網絡層庫來實現的,瀏覽器不須要關心,如 IE 使用的是 WinINET
須要注意 IE 對 cookie 的支持是基於 pre-RFC Netscape draft spec for cookies 的,和標準有些不一樣,在設定 cookie 時會出現轉義不全致使的問題,如在 ie 和 webkit 中會忽略 「=」,不過大部分 web 開發程序(如 php 語言)都會處理好,自行編寫 http 交互時則須要注意
在 IE 中默認狀況下 iframe 中的頁面若是域名和當前頁面不一樣,iframe 中的頁面是不會收到 cookie 的,這時須要經過設置 p3p 來解決,具體能夠察看微軟官方的文檔,加上以下 header 便可
這對於用 iframe 嵌入到其它網站中的第三方應用很重要
頁面的編碼能夠在 http header 或 meta 標籤中指明,對於沒有指明編碼的頁面,瀏覽器會根據是否設置了 auto detect 來進行編碼識別(如在 chrome 中的 View-Encoding 菜單)
關於編碼識別,Mozilla 開源了其中的 Mozilla Charset Detectors 模塊,感興趣的能夠對其進行學習
建議在 http
header 中指定編碼,若是是在 meta 中指定,瀏覽器在獲得 html 頁面後會首先讀取一部份內容,進行簡單的 meta 標籤解析來得到頁面編碼,如 WebKit 代碼中的 HTMLMetaCharsetParser.cpp,能夠看出它的實現是查找 charset 屬性的值,除了 WebKit 之外的其它瀏覽器也是相似的作法,這就是爲什麼 HTML5 中直接使用以下的寫法瀏覽器都支持
須要注意不設定編碼會致使不可預測的問題,應儘量作到明確指定
瀏覽器在加載 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 使得在完成一個請求後能夠不關閉 socket 鏈接,後續能夠重複使用該鏈接發送請求,在 HTTP/1.0 和 HTTP/1.1 中都有支持,在 HTTP/1.1 中默認是打開的
keepalive 在瀏覽器中都會有超時時間,避免長期和服務器保持鏈接,如 IE 是 60 秒
另外須要注意的是若是使用阻塞 IO(如 apache),開啓 keepalive 保持鏈接會很消耗資源,能夠考慮使用 nginx、lighttpd 等其它 web server,具體請參考相關文檔,這裏就不展開描述
pipelining 是 HTTP/1.1 協議中的一個技術,能讓多個 HTTP 請求同時經過一個 socket 傳輸,注意它和 keepalive 的區別,keepalive 能在一個 socket 中傳輸多個 HTTP,但這些 HTTP 請求都是串行的,而 pipelining 則是並行的
惋惜目前絕大部分瀏覽器在默認狀況下都不支持,已知目前只有 opera 是默認支持的,加上不少網絡代理對其支持很差致使容易出現各類問題,因此並無普遍應用
SPDY 是 google 提出的對 HTTP 協議的改進,主要是目的是提升加載速度,主要有幾點:
從實現上看,frame 類(包括 iframe 和 frameset)的標籤是最耗時的,並且會致使多一個請求,因此最好減小 frame 數量
若是要嵌入不信任的網站,可使用這個屬性值來禁止頁面中 js、ActiveX 的執行,能夠參考 msdn 的文檔
對於 html 的 script 標籤,若是是外鏈的狀況,如:
瀏覽器對它的處理主要有 2 部分:下載和執行
下載在有些瀏覽器中是並行的,有些瀏覽器中是串行的,如 IE八、Firefox三、Chrome2 都是串行下載的
執行在全部瀏覽器中默認都是阻塞的,當 js 在執行時不會進行 html 解析等其它操做,因此頁面頂部的 js 不宜過大,由於那樣將致使頁面長時間空白,對於這些外鏈 js,有 2 個屬性能夠減小它們對頁面加載的影響,分別是:
下圖來自 Asynchronous and deferred JavaScript execution explained,清晰地解釋了普通狀況和這 2 種狀況下的區別
須要注意的是這兩個屬性目前對於內嵌的 js 是無效的
而對於 dom 中建立的 script 標籤在瀏覽器中則是異步的,以下所示:
爲了解決 js 阻塞頁面的問題,能夠利用瀏覽器不認識的屬性來先下載 js 後再執行,如 ControlJS 就是這樣作的,它能提升頁面的相應速度,不過須要注意處理在 js 未加載完時的顯示效果
document.write 是不推薦的 api,對於標示有 async 或 defer 屬性的 script 標籤,使用它會致使不可預料的結果,除此以外還有如下場景是不該該使用它的:
簡單來講,document.write 只適合用在外鏈的 script 標籤中,它最多見的場景是在廣告中,因爲廣告可能包含大量 html,這時須要注意標籤的閉合,若是寫入的內容不少,爲了不受到頁面的影響,可使用相似 Google AdSense 的方式,經過建立 iframe 來放置廣告,這樣作還能減小廣告中的 js 執行對當前頁面性能的影響
另外,可使用 ADsafe 等方案來保證嵌入第三方廣告的安全,請參考如何安全地嵌入第三方 js – FBML/caja/sandbox/ADsafe 簡介
將 script 標籤放底部能夠提升頁面展示給用戶的速度,然而不少時候事情並沒那麼簡單,如頁面中的有些功能是依賴 js 的,因此更多的還須要根據實際需求進行調整
js 壓縮可使用 YUI Compressor 或 Closure Compiler
gwt 中的 js 壓縮還針對 gzip 進行了優化,進一步減少傳輸的體積,具體請閱讀 On Reducing the Size of Compressed Javascript
比起 js 放底部,css 放頁面頂部就比較容易作到
使用 @import 在 IE 下會因爲 css 加載延後而致使頁面展示比使用 link 標籤慢,不過目前幾乎沒有人使用 @import,因此問題不大,具體細節請參考 don’t use @import
瀏覽器在構建 DOM 樹的過程當中會同時構建 Render 樹,咱們能夠簡單的認爲瀏覽器在遇到每個 DOM 節點時,都會遍歷全部 selector 來判斷這個節點會被哪些 selector 影響到
不過實際上瀏覽器通常是從右至左來判斷 selector 是否命中的,對於 ID、Class、Tag、Universal 和 Page 的規則是經過 hashmap 的方式來查找的,它們並不會遍歷全部 selector,因此 selector 越精確越好,google page-speed 中的一篇文檔 Use efficient CSS selectors 詳細說明了如何優化 selector 的寫法
另外一個比較好的方法是從架構層面進行優化,將頁面不一樣部分的模塊和樣式綁定,經過不一樣組合的方式來生成頁面,避免後續頁面頂部的 css 只增不減,愈來愈複雜和混亂的問題,能夠參考 Facebook 的靜態文件管理
如下整理一些性能優化相關的工具及方法
以前提到的 http://www.browserscope.org 收集了各類瀏覽器參數的對比,如最大連接數等信息,方便參考
Navigation Timing 是還在草案中的獲取頁面性能數據 api,能方便頁面進行性能優化的分析
傳統的頁面分析方法是經過 javascript 的時間來計算,沒法獲取頁面在網絡及渲染上所花的時間,使用 Navigation Timing 就能很好地解決這個問題,具體它能取到哪些數據能夠經過下圖瞭解(來自 w3c)
目前這個 api 較新,目前只在一些比較新的瀏覽器上有支持,如 Chrome、IE9,但也佔用必定的市場份額了,能夠如今就用起來
yahoo 開源的一個頁面性能檢測工具,它的原理是經過監聽頁面的 onbeforeunload 事件,而後設置一個 cookie,並在另外一個頁面中設置 onload 事件,若是 cookie 中有設置且和頁面的 refer 保持一致,則經過這兩個事件的事件來衡量當前頁面的加載時間
另外就是經過靜態圖片來衡量帶寬和網絡延遲,具體能夠看 boomerang
本文出自 「百度技術博客」 博客,請務必保留此出處 http://baidutech.blog.51cto.com/4114344/746830