這是關于《高性能網站建設指南》的讀書筆記。
黃金法則
只有10%-20%的最終用戶響應時間花在了下載HTML文檔上,其余80%-90%的時間花在下載頁面中所有的組件上。
規則一:減少HTTP請求數
1. CSS Sprites
將多個圖片定位到整合成為一個圖片,然后在用CSS來定位顯示。同時還提到了圖片熱點(map標簽)和data: URL模式。(實際生產中,應該極少會用到后邊兩種)
2. 合并腳本和樣式表
顧名思義,就是將外聯的所有腳本和樣式表都整合成一個腳本和樣式表。但是,需要注意的是,對于模塊化的代碼,將所有的JavaScript和CSS分別合并成為一個文件在開發環境中很難完成。例如,一個頁面可能需要script1、script2和script3,而另一個文件可能需要script1、script3和script5。解決的方法是遵守編譯型語言的模式,保持JavaScript模塊化,而在生成過程中從一組特定的模塊中生成一個目標文件。
伴隨的代價是代碼的復用率降低了(書中并沒有提到),比如第一個頁面將script1、script2和script3組合成為一個js文件,第二個頁面將script1、script3和script5組合成為一個文件,顯然代碼script1和script3都被重新加載了一邊。當然,作為文本的js文件,尤其是在壓縮之后,一般情況下,重復加載的模塊所帶來的文件大小的增加應該是可以忽略的。
規則二:使用內容發布網絡
你的用戶肯能遍布世界的各個角落,即使同一臺服務器,不同用戶的頁面響應時間也是不一樣的,有些情況下會明顯的影響到用戶體驗。一個解決方法是,采取分布式的構架重新設計你的web程序,像谷歌在全球的不同地域部署服務器一樣。但是,這樣巨大的開銷和任務。另一個簡單的解決方法就是,將組件web服務器從應用服務器中分離開來,托管于一些內容發布網絡。
缺點也是顯而易見,你的頁面響應時間會受到托管服務器的影響,如果托管服務器的性能下降,你的網站性能也會下降。并且你也無法直接控制服務器組件,例如修改HTTP響應頭必須通過服務提供商來完成,而不是你的團隊。
規則三:添加Expires頭
web服務器通過Expires頭來告訴web客戶端它可以使用一個組件的當前副本,直到指定的時間為止。如下:
Expires Thu, 15 Apr 2010 20:00:00 GTM
它告訴瀏覽器可以使用改組件的副本一直持續到2010年4月15號。
Expires 的缺點是過期的日期需要經常檢查,一旦這一天到來以后,需要在服務器中配置一個新的日期。不過,在HTTP1.1中提供了另外的字段來完成同樣的功能。在HTTP1.1中,Cache-Control使用max-age來指定組件被緩存多久,它以秒為單位。同樣避免了額外的HTTP請求。例如:
Cache-Control: max-age=315360000
一個最佳的實踐是,長久的Expires應該包含任何不經常變化的組件,包括腳本、樣式表和Flash組件。
同時,我們也應該注意到,當我們配置組件更新時,在直到過期日期之間,瀏覽器都不會檢查任何更新。為了確保用戶能夠獲取最新的版本,需要在所有的HTML頁面中修改組件的文件名。
規則四:壓縮組件
對樣式和樣式表等組件進行壓縮將會減少傳輸時間。壓縮組件的一種方式是,刪除文檔之間的空格和注釋,甚至縮短JS文件中的變量名等,現在有很多工具都可以做到這一點。另一種效果顯著的方法是壓縮傳輸的文件,從HTTP1.1開始,客戶端可以通過Accept-Encoding頭來標識對壓縮的支持:
Accept-Encoding: gzip, deflate
web服務器通過Content-Encoding頭來通知客戶端采用了何種壓縮:
Content-Encoding: gzip
其中,gzip是目前最有效和最流行的壓縮方式。
規則五:將樣式表放在頂部
通常組件的加載是按照文檔中的出現的順序來加載的,所以將不需要立刻使用的樣式(比如用戶點擊彈出對話框的樣式)放在文檔的末尾來加載,就可以獲得一個較好的體驗效果。但是,這個結論是錯誤的。
在IE8及更早的IE瀏覽器中,頁面的逐步呈現會在樣式表下載完成前被阻止,因為在樣式表加載完成之前就構建呈現樹是沒有必要的。這樣做帶來的結果是,HTML在加載時不會提供任何視覺反饋,一直等到樣式加載完成后,然后才呈現出整個頁面,也就是所說的“白屏”,會然用戶感覺到“緩慢”。
在IE9和其他瀏覽器已經沒有“白屏”問題,即使樣式放在底部,頁面也可以逐步呈現,但是樣式加載和解析之后,呈現的文字和圖片就需要用新的樣式進行重繪,這同樣造成了閃爍,依舊是一種不友好的用戶體驗。
另外,W3C標準明確說明應該把樣式放在HTML文檔的頭部。
規則六:將腳本放在尾部
腳本在加載時會阻塞所有的并行下載,因此所有位于腳本以下的內容的逐步呈現都會被阻止,因此需要將腳本放在文檔的尾部。
腳本會阻塞并行下載原因有二。其一,腳本可能使用了document.write來修改頁面的內容,因此瀏覽器不得不等待,以確保頁面的正確布局。其二,為了保證腳本能夠按照正確的順序執行。如果同時并行下載多個腳本,就無法保證響應是按照特定的順序到瀏覽器的。如果它們之間存在著依賴關系,那么就可能導致JavaScript錯誤。
另外一種建議就是使用延遲(Defferred)腳本。Defer 屬性表明腳本可以延遲加載,因此瀏覽器可以繼續進行呈現。不過,在Firefox中,即便是延遲腳本也會阻塞呈現和并行下載。
規則七:避免使用CSS表達式
IE瀏覽器支持CSS表達式,而其他瀏覽器只會簡單的忽略。CSS表單式的樣式如下:
with: expreesion(document.body.clientWidth < 600 ? "600px" : "auto");
expreesion接受一個JavaScript表達式。expression真正的問題在于表達式可能會被極度頻繁的求職,例如上邊的例子,求值不僅發生在瀏覽器窗口大小變化時,而每一次鼠標的移動都會造成表達式求值計算,如果一個文本輸入框獲得焦點,那么甚至會因為反復求值造成瀏覽器奔潰。
規則八:使用外部JavaScript和CSS
純粹而言,內聯樣式更快一些,因為不僅合并后文件大小更小一些,更重要的是有效減少了HTTP請求。但是考慮到JavaScript文件和CSS文件會被緩存,這時外部JS和CSS文件就會帶來性能收益。
規則九: 減少DNS查詢
Internet是通過IP地址來查找服務器的,這意味著瀏覽器中輸入URL在發送請求前會有一次域名的解析過程。
瀏覽器和操作系統各自都有自己的DNS緩存,如果請求的DNS存在在緩存中,那么便不需要向上級DNS解析器發起請求。而且每一個DNS記錄都有一個緩存時間(Time-to0live, TTL)值。
減少頁面中主機名的數量會減少DNS查找,但帶來的另一個潛在的問題是,同時會減[圖片]少了頁面中并行下載的數量。書中給出的建議是,如果頁面中有大量的組件,那么將這些組件放到至少2個,但不要超過4個主機名下。這是在減少DNS查找和允許高度并行下載之間做出很好的權衡。
規則十:精簡JavaScript
精簡是從代碼中移除不必[圖片]要的字符以減小其大小,而混淆則在精簡的基礎上,還修改部分代碼,一般而言,函數和變量名會被轉換成為更短的字符。
混淆的目的是增加對代碼進行反向工程的難度,對于精簡而言,優點是可以進一步減小代碼。其缺點是,混淆過程本身可能引入錯誤,維護和調試也會變得更加困難。
因此,采取混淆還是精簡,最終的決定需要考慮混淆能夠帶來額外的代碼大小的減少量。一般情況下,建議采用精簡而不是混淆。規則四提到的壓縮能夠極大的減小傳輸代碼的大小,大約70%,所以壓縮會被精簡更加有效。在采用了壓縮的情況下,精簡和混淆之間的區別會進一步減小,幾乎一樣。
規則十一:避免重定向
重定向是用于將用戶從一個URL重新路由到另一個URL。重定向會延遲整個HTML文檔的傳輸。下面介紹幾種常見的重定向以及解決方案。
- 缺少結尾的斜線
在URL必須出現斜線時沒有出現就可能導致重定向,例如,訪問http://astrology.yahoo.com/astrology 時,就會產生一個301響應,其中包好了一個到 http://astrology.yahoo.com/astrology/ 的重定向。這是一種最為浪費、發生的也很貧乏的重定向。
解決的方法,一般通過配置web服務器就能解決這個問題。
- 連接網站
這種情況一般發生在網站被重寫導致新的URL和舊的URL不一樣,另外也包括將一個網站的不同部分連接起來等。重定向讓連接兩個網站很簡單,而且只需要很少的額外代碼。
解決的方法也較多,比如,如果后端位于同一臺服務器上,則它們的代碼可能自己均能夠連接,如果域名變了,則可以使用一個CNAME讓兩個主機名指向相同的服務器。
- 跟蹤內部流量
重定向經常用于跟蹤內部的流量。解決方案比較復雜,可以參考書中。
規則十二:移除重復的腳本
導致一個腳本的重復有連個重要的因素——團隊的大小和腳本的數量。而重復腳本損傷性能的方式有兩種——不必要的HTTP請求和執行JavaScript所浪費的時間。
規則十三:配置ETag
實體標簽(Entity Tag)是 web 服務器和瀏覽器用于確認緩存組件的有效性的一種機制。例如,請求如下:
GET /i/yahoo.gif HTTP 1.1
Host: us.yimg.com
服務器響應如下:
HTTP 1.1 200 OK
Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
ETag: "10c24bc-4ab-457e1c1f"
Content-Length: 1195
Etag 的加入為驗證組件提供了比最新修改日期更為靈活的機制。例如,如果實體依據 User-Agent 或 Accept-Language 頭而改變,實體的狀態可以反映在 Etag 中。
此后,如果瀏覽器必須驗證一個組件,他會使用 If-None-Match 頭將 ETag 傳回原始服務器。
GET /I/YAHOO.GIF HTTP 1.1
Host: us.yimg.com
If-Modified-Since: Tue, 12 dec 2006 03:03:59 GMT
If-None-Match: "10c24bc-4ab-45e1c1f"
如果 ETag 匹配的,就會返回 304 狀態碼。
HTTP 1.1 304 Not Modified
ETage 的問題在于,通常使用組件的某些特性來構造它,這些屬性對于特定的、寄宿了網站的服務器來說是唯一的。當瀏覽器從一臺服務器上獲取了原始組件,之后,又向另一臺不同的服務器發起條件 GET 請求時, ETag 是不會匹配的——而對于使用服務器集群來處理請求的網站來說,這是很常見的一種情況。依據 HTTP1.1 規范,如果請求中同時出現了 If-Modified-Since 和 If-None-Match 這兩個頭,除非請求中的條件頭字段全部一致,否則服務器禁止返回 304。
解決的方法是配置或者移除ETag。通過配置 web 服務器或者通過后端應用程序直接控制,使得訪問不同服務器上相同的組件時,返回的 ETag 是相同的。當然更加直接的方法是,不使用 ETag。
規則十四:使 Ajax 可緩存
確保 Ajax 請求遵循性能指導,尤其應具有長久的 Expires 頭。
后記
本片文章只是簡單的筆記,原書中提供了大量的實例從數據上去論證這些規則,如果需要有深刻的了解,應該去閱讀原書。