前端性能優化的14條法則

文章是高性能網站建設指南(Steve Souders)的總結。

性能黃金法則:

只有10%~20%的最終用戶響應時間花在了下載HTML文檔上。其餘的80%~90%時間花在了下載頁面中的所有組件上。

在這之後是14條提升性能的法則。這些規則按照常規的優先級順序列出。特定的規則,適用性可能不同。所以不同的網站需要合理的選取規則。

規則1—-減少HTTP請求

圖片地圖

圖片地圖允許你在一張圖片上關聯多個URL。想必多張圖片(多個URL)而言,一張圖片只有一個HTTP請求。

圖片地圖實現方法有兩種,一種是服務器端圖片地圖,一種是客戶端圖片地圖

  • 服務器端圖片地圖是將所有點擊提交到同一個URL,向其傳遞x、y座標。web應用程序將座標映射爲適當的操作。

  • 客戶端圖片地圖是通過map標籤。下面是w3c的示例:

<img src="planets.jpg" border="0" usemap="#planetmap" alt="Planets" />

<map name="planetmap" id="planetmap">
  <area shape="circle" coords="180,139,14" href ="venus.html" alt="Venus" />
  <area shape="circle" coords="129,161,10" href ="mercur.html" alt="Mercury" />
  <area shape="rect" coords="0,0,110,260" href ="sun.html" alt="Sun" />
</map>

客戶端圖片地圖靈敏度太差,只適合矩形,而且座標易出錯。(我是沒用過)

CSS Sprites(CSS雪碧圖)

一句話概括就是將很多張小圖片通過ps放到一張圖片上。這樣通過設置背景位置(background-position)在不同位置顯示合適的圖片。

同樣,使用雪碧圖也可以減少大量HTTP請求。

另一個令人驚奇的優點是,它還降低了下載量。很多人會認爲合併後的圖片比分離的圖片的總和要大,因爲合併後的圖片中包含有附加的空白區域。實際上,合併後的圖片會比分離後的圖片的總和要小,這是因爲它降低了圖片自身的開銷(顏色表、格式信息,等等)。(對圖片要求不高,我使用iconfont)

內聯圖片

通過使用data:URL模式可以在web頁面中包含圖片但無需任何額外的HTTP請求。

data:<mediatype>[;base64],<data>

如:

background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7");

合併腳本和樣式表

  • 多個腳本合併爲一個腳本
  • 多個樣式表合爲一個樣式表

對於模塊化代碼,合理選擇組合是有必要的。

規則2—-使用內容分發網絡(CDN)

如果應用程序Web服務器離用戶更近,則一個HTTP請求的相應時間將會被縮短。
另一方面,如果組件web服務器離用戶更近,則多個HTTP請求的相應時間將會被縮短。

優點

  • 內容分發網絡(CDN)是一組分佈在多個不同地理位置的web服務器,用於更加有效地向用戶發佈內容。通常只在討論性能問題時會提到它的性能,但它還節省成本。

  • 除了縮短響應時間以外,CDN還可以帶來其他優勢。他們的服務包括備份、擴展存儲能力和進行緩存。CDN還有助於緩和web流量峯值的壓力,如在獲取天氣或股市新聞、瀏覽流行的體育或娛樂事件時。

缺點

  • 依賴CDN的一個缺點是你的響應時間可能會受到其他網站—-設置可能是你的競爭對手的影響。CDN服務提供商在其所有客戶之間共享其web服務器組。

  • 無法直接控制組件服務器。比如修改HTTP響應頭必須通過服務提供商來完成,而不是由你的工作團隊完成。

  • 如果CDN性能下降,你的工作質量也會隨之下降(不過你可以使用多個CDN服務提供商)。

規則3—-添加Expires頭

今天的web頁面都包含了大量的組件,並且數量在不斷增長。頁面的初訪者會進行很多HTTP請求,但通過使用一個長久的Expires頭,使這些組件可以被緩存。這會在後續的頁面瀏覽中減少不必要的HTTP請求。長久的Expires頭最常用與圖片,但應該將其用在所有組件上,包括腳本、樣式表的Flash。

Expires頭

如下響應頭(百度首頁的某張圖片):
Response Headers

Accept-Ranges:bytes
Age:792098
Cache-Control:max-age=2592000
Connection:keep-alive
Content-Length:0
Content-Type:image/png
Date:Mon, 06 Feb 2017 06:36:17 GMT
ETag:"5881c862-664"
Expires:Mon, 27 Feb 2017 02:34:38 GMT
Last-Modified:Fri, 20 Jan 2017 08:20:50 GMT
Ohc-Response-Time:1 0 0 0 0 0
Server:bfe/1.0.8.13-sslpool-patch

指定了Expires,即過期時間。但是同時又指定了Cache-Control,這時情況需要另算了。

max-age和mod_expires

HTTP 1.1引入了Cache-Control頭來客服Expires頭的限制。因爲Expires頭使用一個特定的時間,它要求服務器和客戶端的時鐘嚴格同步。另外,過期時間需要經常檢查,並且一旦這一天到來了,還需要在服務器配置中提供一個新的日期。

換一種方式,Cache-Control使用max-age指定組件被緩存多久(單位是秒)。如果從組件被請求開始過去的秒數少於max-age,瀏覽器就是用緩存的版本,這就避免了額外的HTTP請求。如上述的max-age頭將刷新窗設置爲30天。

使用帶有max-age的Cache-Control可以消除Expires的限制,但對於不支持HTTP 1.1的瀏覽器(還有嗎?),如果仍然希望提供Expires頭,可以同時提供兩個響應頭–Expires和Cache-Control。如果兩者同時出現,HTTP規範規定max-age指令將重寫Expires頭。然而,如果你很盡職盡責,你仍然需要擔心Expires帶來的時鐘同步和配置維護問題。

幸運的是,mod_expires Apache模塊使你在使用Expires頭時能夠像max-age那樣以相對的方式設置日期。

關於Cache-Control,不同的操作影響是不同的。見:

http://www.laruence.com/2010/03/05/1332.html

空緩存和完整緩存

“空緩存”和“完整緩存”指的是與頁面相關的瀏覽器緩存的狀態。如果你的頁面中的組件沒有放在緩存中,則緩存爲“空”。瀏覽器的緩存可能包含來自其他網站的組件,但這對你的頁面沒有任何幫助。反之,如果你的頁面中的可緩存組件都在緩存中,則緩存是“完整的”。

chrome中,如何找到緩存:

  • chrome://cache
  • chrome://version => 個人資料路徑 => cache/media cache

不僅僅是圖片

爲圖片使用長久的Expires頭非常普遍,但這一最佳實踐不應該僅限於圖片。長久的Expires頭應該包含任何不經常變化的組件,包括腳本、樣式表和Flash組件。但是,HTML文件不應該使用長久的Expires頭,因爲它包含動態內容,這些內容需要經常被更新。

修訂文件名

如果緩存的文件改變了,那麼如何更新呢?當出現了Expires/Cache-Control頭時,直到過期時間爲止一直會使用緩存的版本。瀏覽器不會檢查任何更新,直到過了過期日期。這也是爲什麼使用Expires/Cache-Control頭能夠顯著減少相應時間—-瀏覽器直接從硬盤上讀取組件而無需生成任何HTTP請求。

爲了確保用戶能夠獲取組件的最新版本,需要在所有HTML頁面中修改組件的文件名。

最有效的解決方案是修改其所有鏈接,這樣,全新的請求將從原始服務器下載最新的內容。

如果使用PHP、Perl等動態語言生成HTML頁,一種簡單的解決方案是爲所有組件的文件名使用變量。使用這種方法,在頁面中更新文件名時只需要簡單地在某個地方修改變量(如改變版本號xxx_1.1.js)。

一個具有長久的Expires頭的組件將會被緩存,在後續請求中瀏覽器直接從硬盤上讀取它,避免HTTP請求。然而,如果一個組件沒有長久的Expires頭,它仍然會存儲在瀏覽器的緩存中。在後續請求中,瀏覽器會檢查緩存並發現組件已經過期(HTTP術語稱爲 陳舊)。爲了提高效率,瀏覽器會向原始服務器發送一個條件GET請求。如果組件沒有改變,原始服務器可以免於發送整個組件,而是發送一個很小的頭,告訴瀏覽器可以使用其緩存的組件。

SSL

如果設置Cache-Control: no-store表明該相應因爲數據隱私原因不會被緩存,這是相應不會被寫到磁盤上。但是HTTP規範警告說不要依賴這一機制來確保數據的隱私性,惡意的或危險的緩存會完全忽略這個頭。

處理數據隱私性更好的方法是使用安全通信協議如安全套接字層(Secure Layer, SSL)。SSL相應是可緩存的(在ff中只能用於當前瀏覽器回話),因此它提供了一種妥協—-在確保數據隱私性的同時在當前會話中緩存相應以改善用戶體驗。

規則4—-壓縮組件

本規則通過減小HTTP響應的大小來減少相應時間。如果HTTP請求產生的響應包很小,傳輸時間就會減少,因爲只需要將很小的包從服務器傳遞到客戶端。

壓縮是如何工作的

從HTTP 1.1開始,Web客戶端可以通過HTTP請求中的Accept-Encoding頭來標識對壓縮的支持。
Request Headers

GET http://google.com/ HTTP/1.1
Host: google.com
Proxy-Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
X-Client-Data: CKy1yQEIirbJAQiltskBCMG2yQEIqZ3KAQ==
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie: 省略

如果web服務器看到請求中有這個頭,就會使用客戶端列出的方法中的一種來壓縮相應。web服務器通過相應中的Content-Encoding頭來通知web客戶端。

Content-Encoding:gzip

gzip是目前最流行和最有效的壓縮方法。另一種壓縮是deflate,但效果略遜且不太流行。

壓縮什麼

  • HTML文檔、樣式表、腳本(其實任何文本相應都可以)
  • 圖片的PDF不應該被壓縮,因爲它們本來已經被壓縮了。

配置

通過Apache配置文件、服務器端設置頭部。

代理緩存

當瀏覽器直接與服務器進行通信時,一切都可以正常工作。
當瀏覽器通過代理來發送請求時,情況就複雜了。考慮一下情況:

  1. 對於某個url、一個請求來自於支持gzip壓縮的瀏覽器。代理中沒有緩存,會將請求轉發到服務器。此時服務器響應一個壓縮的文件,代理緩存這個文件,併發給服務器。
  2. 同樣對於這個url,一個請求來自於**不支持**gzip壓縮的瀏覽器,代理仍然將緩存的壓縮文件返回給瀏覽器。這樣就出問題了。

解決這一問題的方法是在web服務器的相應中添加Vary頭。web服務器可以告訴代理根據一個或多個請求頭來改變緩存的響應。由於壓縮的決定是基於Accept-Encoding頭的,因此需要在服務器的Vary響應頭中包含Accept-Encoding。

這使得代理會緩存響應的多個版本,爲Accept-Encoding請求頭的每個值緩存一份。再根據不同的Accept-Encoding值返回給瀏覽器不同的文件。

如果配置瀏覽器白名單來決定響應是否壓縮(通常在配置文件中),需要:

Vary: Accept-Encoding, User-Agent

不同User-Agent值非常多(目前幾大主流瀏覽器還是可以數過來的),這樣設置白名單不是很合理。

規則5—-將樣式表放在頭部

逐步呈現

關心前端性能的工程師們都希望頁面能逐步地加載,也就是說,我們希望瀏覽器能夠儘快顯示內容。
將樣式表放在文檔底部會導致在瀏覽器中阻止更多內容呈現。爲避免樣式變化時重繪頁面中的元素,瀏覽器會堵塞內容逐步呈現。在瀏覽器和用戶等待位於底部的樣式表時,瀏覽器會延遲顯示任何可視化組件,我們稱之爲白屏

白屏

在某些瀏覽器中將CSS樣式表放在底部會導致白屏。爲了避免白屏,我們應該將樣式表放在文檔頂部的HEAD中(頁面逐步呈現)。將樣式表放在文檔中有兩種方式,使用link標籤和@import規則。

<link rel="stylesheet" href="style1.css">
<style>
  @import url("style2.css")
</style>

link的優點

  • 一個style塊可以包含多個@import規則,但@import規則必須放在所有其他規則之前(否則不會加載)。

  • link除了語法簡單外,還能帶來性能上的收益。@import可能會導致白屏,即使放在HEAD中也是如此。
    使用@import規則會導致組件下載的無序性(可能最後下載導致白屏)。

無樣式內容的閃爍

如果樣式表仍在加載,構建呈現樹就是一種浪費,因爲所有樣式表加載並解析完畢之前無需繪製任何東西。否則,在其準備好之前顯示內容會遇到FOUC(無樣式內容的閃爍, Flash of Unstyled Content)問題。

白屏是瀏覽器在嘗試修改前端工程師所犯的錯誤—-將樣式表放在文檔比較靠後的位置。白屏是對FOUC問題的彌補。瀏覽器可以延遲呈現,直到所有的樣式表都下載完之後,這就導致了白屏。反之,瀏覽器可以逐步呈現,但要承擔閃爍的風險。這裏沒有完美的選擇。所以作爲前端工程師,所需要做的就是將樣式表放在HEAD中。

規則6—-將腳本放在底部

腳本會帶來兩個問題:

  • 腳本會堵塞並行加載,無論放在哪裏都是如此
  • 對於位於腳本以下的內容,逐步呈現都被堵塞了。將腳本放在頁面越靠下的地方,意味着越多的內容能夠逐步地呈現。

並行下載

瀏覽器會並行地執行HTTP請求。(F12->Network->Timeline - Start Time)
HTTP 1.1規範建議瀏覽器從每個主機名並行地下載兩個組件。很多Web頁面需要從一個主機名下載所有組件。

如果一個Web頁面平均地將組件分別放在兩個主機名下,整體響應時間可以減少大約一半(可以並行下載四個組件)。

每個主機並行下載兩個組件只是一個建議。依據瀏覽器不同而不同。

http://www.iefans.net/qingqiu-bingfa-lianjieshu-xianzhi/

前端工程師預期依賴用戶來修改瀏覽器設置,不如簡單地使用DNS將組件放到多個主機名中。但使用並行下載不是沒有開銷的,這取決於你的帶寬的CPU速度,過多的並行下載反而會降低性能。YaHoo!的研究表明,使用兩個主機名比使用1、4/10個主機名能帶來更好的性能(主機數目多會增加DNS解析耗時)。

腳本堵塞下載

並行下載的優點是很明顯的。然而,在下載腳本時並行下載實際上是被禁用的(現代瀏覽器一般上是可以的,自己觀察一下)。即使使用了不同的主機名,瀏覽器也不會啓動其他的下載。

  • 其中一個原因是,腳本可能使用document.write來修改頁面內容,因此瀏覽器會等待,以確保頁面能夠恰當地佈局。
  • 另一個原因是爲了保證腳本順序可以按照正確的順序執行。如果並行下載過個腳本,就無法保證相應是按照特定順序到達瀏覽器的。

最差情況 將腳本放在頂部

會帶來兩個問題:

  • 堵塞頁面內容的呈現
  • 堵塞其他組件的下載

正確放置

規則7—-避免CSS表達式

CSS表達式是動態設置CSS屬性的強大(但危險)方法。Internet Explorer從第5個版本開始支持CSS表達式.

  • 背景色輪換:
background-color: expression((new Date()).getHours() % 2 ? '#000' : '#fff');
  • 頁面最小寬度
width: expression( document.body.clientwidth < 600 ? '600px' : 'auto'); // IE
min-width: 600px; // other

用其他方式代替CSS表達式。

規則8—-使用外部JavaScript和CSS

內聯VS外置

純粹而言,內聯JavaScript和CSS會快一些,因爲減少了HTTP請求。但是在實際中,還是使用外部文件會產生比較快的頁面。這是因爲JavaScript和CSS文件有機會被瀏覽器緩存起來。

頁面查看

每個用戶產生的頁面查看越少,內聯JavaScript和CSS的論據越強勢。反之,如果用戶能夠產生很多的頁面查看,瀏覽器很可能將(具有長久Expires/max-age)外部文件放在緩存中。使用外部文件帶來的收益會隨着每用戶查看頁面次數的增加而增加。

組件重用

如果網站中每個頁面都使用了相同的JavaScript和CSS,使用外部文件可以提高這些組件的重用率。在這種情況下外部文件更具有優勢,因爲當用戶在頁面間導航時,JavaScript和CSS組件已經位於瀏覽器的緩存中了。相反,如果沒有任何兩個頁面共享相同的JavaScript和CSS,重用率就會很低。

在實際情況中,需要在引用外部文件(可以緩存)和內聯文件(減少HTTP請求)之間找到一個平衡點。一般來說,將頁面劃分成幾種不同的頁面類型,然後爲每種類型創建單獨的腳本和樣式表。這比維護一個單獨的文件要複雜,但通常比爲每個頁面維護不同的腳本和樣式表要容易,並且對於給定的任意頁面都只需下載很少多餘的JavaScript和CSS。

加載後下載

對於作爲多次頁面查看中的第一次的主頁,我們希望爲主頁內聯JavaScript和CSS,但又能爲所有後續頁面查看提供外部文件。這可以通過在主頁加載完成後動態下載外部組件來實現(通過load事件)。這能夠將外部文件放到瀏覽器的緩存中以便用戶接下來訪問其他頁面。

<script>
window.onload = function() {
  setTimeout(function(){
    var scriptEle = document.createElement('script');
    scriptEle.src = 'script1.js';
    document.body.appendChild(scriptEle);

    var linkEle = document.createElement('link');
    linkEle.rel = 'stylesheet';
    linkEle.type = 'text/css';
    linkEle.ref = 'css1.css';
    document.head.appendChild(linkEle);
  }, 1000)
}
</script>

動態內聯

// nodejs
http.createServer(function(req, res) {
    res.write('<script>alert(1)</script>');
    res.end();
})

規則9—-減少DNS查找

Internet通過IP地址來查找服務器的。DNS將主機名映射到IP地址上。

然而,DNS也是開銷。通常瀏覽器查找一個給定的主機名的IP地址要花費20~120毫秒。在DNS查找完成之前,瀏覽器不能從主機名那裏下載任何東西。

DNS緩存和TTL

查看瀏覽器的DNS緩存(Chrome中在地址欄輸入chrome://net-internals/#dns)。

在用戶請求了一個主機名之後,DNS信息會留在操作系統的DNS緩存中(windows是“DNS Client”服務),之後對於該主機名的請求將無需進行過多的DNS查找,至少短時間內不需要。

瀏覽器也會擁有自己的緩存,和操作系統的緩存相分離。只要瀏覽器在其緩存中保留了DNS記錄,它就不會麻煩操作系統來請求這個記錄。只有當瀏覽器緩存丟棄了這個記錄時,纔會向操作系統詢問地址。

服務器通過設置可以表明記錄被緩存多久。查找返回的DNS記錄包含了一個存活時間(Time-to-live,TTL)值。該值告訴客戶端可以對該記錄緩存多久。

儘管操作系統緩存會考慮TTL值,但瀏覽器通常會忽略該值,並設置它自己的時間限制。此外HTTP協議中的Keep-Alive特性可以同時覆蓋TTL和瀏覽器的時間限制。換句話說,只要瀏覽器的Web服務器愉快地通信着,並保持着TCP連接打開狀態,就沒有理由進行DNS查找。

瀏覽器對緩存的DNS記錄的數量也有限制,而不管緩存記錄的時間。

windows上通過“DNS Client”服務管理DNS緩存。可以使用ipconfig命令來查看和刷新DNS Client服務。

ipconfig /displaydns
ipconfig /flushdns

重新啓動也可以情況DNS Client緩存。重啓瀏覽器會清空瀏覽器緩存,但不會清空DNS Client服務緩存。

在chrome中通過chrome://net-internals/#dns可查看dns記錄緩存情況,有意思的是,chrome中的緩存似乎總是長期的,只有數量限制。
這裏寫圖片描述

減少DNS查找

減少DNS查找意味着使用更少的主機名。使用更少的主機名又意味着減少了並行下載數量。如何權衡?
作者給出的建議是講這些組件放在至少兩個,但不要超過4個主機名下。這是在減少DNS查找和允許高度並行下載之間作出的很好的權衡。

規則10—-精簡JavaScript

精簡

精簡是從代碼中移除不必要的字符以減小其大小,進而改善加載時間的時間。在代碼被精簡後,所有的註釋以及不必要的空白字符都將被移除。這能減小文件體積。

混淆

混淆是可以應用在源代碼上的另外一種優化方式。和精簡一樣,它也會移除註釋和空白,同時它還會改寫代碼。作爲改寫的一部分,函數和變量的名字將會被轉換爲更短的字符串,這時的代碼更加精煉,也更難閱讀。通常這樣做是爲了增加對代碼反向工程的難度,但這對提高性能也有幫助,因爲這筆精簡更能減少代碼的大小。

混淆的缺點

  • 由於混淆更加複雜,混淆過程本身很有可能引入錯誤。
  • 由於混淆會改變JavaScript符號,一次需要對任何不能改變的符號(例如API函數)進行標記,防止混淆器修改他們。
  • 經過混淆的代碼很難閱讀。這使得在產品環境中調試更加困難。

精簡內聯腳本

這也是可行的。

壓縮和精簡

gzip壓縮可以減少文件體積70%左右。使用精簡後再使用gzip壓縮會帶來更高的效率。

精簡CSS

帶來的節省小於精簡JavaScript。

  • 移除註釋和空白
  • 合併相同的類,移除不必要的類
  • 使用縮寫(用#666代替#666666)
  • 移除不必要的字符串(0代替0px)

規則11—-避免重定向

重定向用於將用戶從一個URL重新路由到另一個URL。重定向有很多種,301和302是最常用的兩種。通常對HTML文檔進行重定向,不過也可以用於組件(圖片,腳本等)。重定向的原因有很多,包括網站重新設計、跟蹤流量、記錄廣告點擊和建立易於記憶的URL。但無論如何,重定向會使得頁面變慢

當web服務器想瀏覽器返回一個重定向時,相應中就會擁有一個範圍在3xx的狀態碼。這表示用戶代理必須執行進一步操作才能完成請求。以下是幾種3xx狀態碼—-
- 300 Multiple Choices(基於Content-Type)
- 301 Moved Permanently
- 302 Moved Temporarily(亦稱Found)
- 303 See Other(對302的說明)
- 304 Not Modified
- 305 Use Proxy
- 306(不再使用)
- 307 Temporary Redirect(對302的說明)

“304 Not Modified”並不是真正的重定向—-它是用來相應條件GET請求,避免下載已經存在於瀏覽器緩存中的數據。

301和302是用的最多的。303和307是在HTTP 1.1規範中添加的,用來澄清對302的使用(濫用),但是幾乎沒人使用303和307。下面是301響應頭的一個示例:

HTTP 1.1 301 Moved Permanently
Location: http://www.baidu.com
Content-Type: text/html

瀏覽器會自動將用戶帶到Location字段所給出的URL。重定向所需要的信息都出現在這個頭中了。相應提通常是空的。301和302相應在實際中都不會被緩存,除非有附加的頭—-如Expires或Cache-Control。

在meta refresh標籤中可以在其content屬性所指定的秒數後重定向用戶:

<meta http-equiv="refresh" content="5; url=http://www.baidu.com">

JavaScript也可以進行重定向,將document.location設置爲期望的URL即可。如果你必須要進行重定向,最好的技術是使用標準的3xx HTTP狀態碼。

重定向時如何損傷性能的

在重定向完畢並且HTML文檔下載完畢之前,沒有任何東西顯示給用戶。重定向延遲了HTML的加載。

重定向之外的其他選擇

缺少結尾斜線

這是最長髮生的重定向,也是開發人員最容易忽略的。因爲它有充分的理由—-它允許自動索引(自動轉移到默認的index.html)。並且能夠獲得與當前目錄相關的URL(logo.gif)。然而,很多流行的web站點並不依賴自動索引,而是依賴特定的URL和處理器。另外,URL通常也與根目錄相關而不是和當前目錄相關。

注意當主機名後缺少結尾斜線時不會發生重定向。比如,http://www.baidu.com不會發生重定向。然而,通過檢查請求,會發現URL是包含結尾斜線的。導致自動產生斜線的原因是,瀏覽器在進行GET請求時必須制定一些路徑。如果沒有路徑,例如:http://www.baidu.com,它就會簡單地使用文檔根(/):

GET / HTTP 1.1

當缺少結尾斜線時發送重定向時很多web服務器的默認行爲,包括Apache。

連接網站

想象一下web後端被重寫的情形,這時新的實現中的URL很可能會有所不同。將用戶從舊的URL轉移到新的URL的最簡單的方式就是重定向。
雖然重定向降低了開發的複雜性,但是這也損害了用戶體驗。整合兩個後端還有其他的選擇,但比起簡單地重定向需要做更多的開發工作,不過這樣非但不會損害用戶體驗,還能使之改善。

  • Alias、mod_rewrite、DirectorySlash(都是服務器端配置)要求除URL之外還要提交到一個接口(處理器或文件名),但易於實現。
  • 如果兩個後端在同一個服務器上,則他們的代碼很可能自己就能連接。例如,舊的處理器代碼通過編程調用新的處理器代碼。
  • 如果域名變了,可以使用一個CNAME(一條DNS記錄,用於創建一個域名指向另一個域名的別名)讓兩個主機名指向相同的服務器。如果做到治理點,這裏提到的技術(上面兩點)就是可行的。

跟蹤內部流量

重定向經常用於跟蹤用戶流量的流向。比如一個鏈接被重定向包裝,單擊這個鏈接時,返回一個301相應,Location指向真正的URL。這樣就可以知道用戶的流量去向。

另一種選擇是通過referer日誌來跟蹤流量去向。每個HTTP請求都包含一個URL,表明從哪個頁面發起的請求,也就是引用方(有的時候沒有引用頁,如用戶鍵入URL或使用書籤時)。我通過百度搜索打開一個鏈接,在這個鏈接網頁請求頭中發現:

Referer:https://www.baidu.com/link?url=IZv4coYEvm06hEpRFIDlwAB4faRFbtcX7Krs9YZ3HQCu1K0DdD3st5FuIsqXF-TuQSmngS27uZRwyUJUrt5RO_Gu5eDhB62FOl97vLZqjvK&wd=&eqid=dd07dd000002a56200000004589b210e

使用Refered日誌避免了重定向,也就改善了相應時間。然而,這種方法的難處在於,要相對離開百度的人進行統計,百度就必須分析所有目標網站的日誌進行分析。
對於內部流量—-也就是同一家公司的各個網站之間的流量—-值得通過建立Referer日誌來避免重定向,以此節省響應時間。如果目標網站屬於其他公司,就不可能分析Referer日誌了。

跟蹤出站流量

當你嘗試跟蹤用戶流量時,你會發現鏈接可能將用戶帶離你的網站。在這種情況下,使用Referer就不太現實了。

跟蹤出站流量除重定向之外的選擇是使用信標(beacon)—-一個HTTP請求,其URL包含有跟蹤信息。跟蹤信息可以從信標web服務器的訪問日誌中提取出來。信標通常是一個1px x 1px的透明圖片;不過204相應更優秀,因爲它更小,從不會被緩存,而且絕對不會改變瀏覽器的狀態。

美化URL

使用重定向可以是URL更加美觀並且易於記憶。改變方式參見上述連接網站

規則12—-移除重複腳本

重複腳本

導致重複腳本有兩個重要因素—-團隊大小和腳本數量。不同的團隊都要處理某個東西(比如cookie),於是他們都添加了腳本(cookie.js)。另外,腳本數量衆多,也會造成混淆。

重複腳本損傷性能

重複腳本損傷性能的方式有兩種—-不必要的HTTP請求和執行JavaScript所浪費的時間。

避免重複腳本

  • 在模板系統中實現一個腳本管理模塊。huiv
  • 服務器端插入腳本時檢查。

規則13—-配置ETag

ETag是什麼?

實體標籤(Entity Tag,ETag)時web服務器和瀏覽器用於確認緩存有效性的一種機制。在進入ETag的細節之前,我們先回顧一下組件是如何被緩存和確認的。

Expires頭

瀏覽器下載組建時,會將它們保存到緩存中。下次使用這個組件中,如果這個組件時新鮮的,就從硬盤使用這個組件。是否是新鮮的取決於Expires/Cache-Control頭。

條件GET請求

如果緩存的組件過期了(或者用戶明確重新加載了頁面),瀏覽器再重用它之前必須首先檢查它是否仍然有效。這稱作一個條件GET請求。不幸的是瀏覽器必須執行這個請求執行有效性檢查,但這仍然比簡單地下載所有已過期的組件的效率要高。如果瀏覽器緩存中的組件是有效的(即它能夠和原始服務器上的組件相匹配),原始服務器不會返回整個組件,而是返回一個“304 Not Modified”狀態碼。

服務器在檢測緩存的組件是否和原始服務器上的組件相匹配時有兩種方式:

  • 比較最新修改日期
  • 比較實體標籤

最新修改日期

瀏覽器通過If-Modified-Since請求頭表明瀏覽器緩存的最新日期。
原始服務器通過Last-Modified響應頭來返回組件的最新修改日期。

如果If-Modified-Since和Last-Modified的值一樣,服務器會返回一個304相應,而不會再次傳送需要的數據。
Request Headers

GET /5aV1bjqh_Q23odCf/static/superman/css/super_min_c53cfdce.css HTTP/1.1
Host: ss0.bdstatic.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/css,*/*;q=0.1
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8
If-None-Match: W/"5881c862-63c6"
If-Modified-Since: Fri, 20 Jan 2017 08:20:50 GMT

Response Headers

HTTP/1.1 304 Not Modified
Server: bfe/1.0.8.13-sslpool-patch
Date: Thu, 09 Feb 2017 01:14:24 GMT
Content-Type: text/css
Connection: keep-alive
ETag: W/"5881c862-63c6"
Last-Modified: Fri, 20 Jan 2017 08:20:50 GMT
Expires: Sun, 19 Feb 2017 09:04:01 GMT
Age: 1699234
Cache-Control: max-age=2592000
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Encoding: gzip
Ohc-Response-Time: 1 0 0 0 0 0

可以看出,If-Modified-Since和Last-Modified的值是匹配的。所以使用緩存文件。

實體標籤

ETag提供了另外一種方式,用於檢測緩存中的組件和原始服務器上的組件是否匹配(“實體”是“組件”的另一種稱呼)。ETag在HTTP 1.1引入。ETag是唯一標識了一個組件的特定版本的字符串。唯一的格式約束是該字符串必須用引號引起來。原始服務器使用ETag響應頭來指定組件的ETag。

ETag的加入爲驗證實體提供了比最新修改日期更爲靈活的機制。例如,如果實體依賴User-Agent或Accept-Language頭而改變,實體的狀態可以反映在ETag中。

此後,如果瀏覽器必須驗證一個組件,它會使用If-None-Match傳回原始服務器。如果ETag是匹配的,就會返回一個304狀態碼。如上。

ETag帶來的問題

ETag通常使用組件的某些屬性來構造它,這些屬性對於特定的、寄宿了網站的服務器來說是唯一的。當瀏覽器從一臺服務器上獲取了原始組件後又向另一臺不同的服務器發起條件GET請求時,ETag是不會匹配的—-而對於使用服務器集羣來說,這是很常見的一種情況。默認情況下,對於擁有多臺服務器的網站來說Apache和IIS向ETag中嵌入的數據都會大大地降低有效性驗證的成功率。

Apache1.3和2.x的ETag格式是inode-size-timestamp(上述不是)。文件系統使用inode來存儲諸如文件類型、所有者、組和訪問模式等信息。儘管在多臺服務器上一個文件可能位於相同的目錄、具有相同的文件大小、權限、時間戳等,從一臺服務器到另一臺服務器,其inode仍然是不同的。

如果ETag不匹配會讓用戶下載更多內容,用戶體驗會下降。同時,服務器開銷也更大。如果有10臺服務器,則用戶只有10%的機會得到正確的304相應。其餘90%的機會會得到200響應並下載所有數據。

ETag還降低了代理緩存的效率。代理後面的用戶緩存的Tag經常和代理緩存的ETag不匹配。這導致不必要的請求被髮送到原始服務器。用戶和代理之間不會出現304相應,而是產生兩個又慢又大的200相應—-一個是原始服務器到代理的,一個是從代理到用戶的。ETag的默認格式還可能引入安全性弱點。

更重要的是,If-None-Match比If-Modified-Since具有更高的優先級。你可能希望如果ETag不匹配但最新修改日期是相同的,也能發送一個“304 Not Modified”相應,但實際並不是這樣的。依據HTTP 1.1規範,如果請求中同時出現了這兩個頭,則原始服務器禁止(MUST NOT)返回304(Not Modified),除非請求頭中的條件頭字段全部一致。實際上如果沒有If-None-Match反而會更好一些。

ETag—-用還是不用

如果你在多臺服務器上寄宿你的網站,而且使用的是具有默認ETag配置的Apache或IIS,用戶將面對緩慢的界面、服務器會具有很高的負載、會消耗大量的帶寬、並且代理也不能有效地緩存內容。即使有長久的Expires/Cache-Control頭,一旦用戶“重新加載”或者“刷新”頁面,還是會產生條件GET請求。ETag還是要被驗證。

一種選擇是對ETag進行配置,以利用其靈活的驗證能力。如:

<?php
if(strops($_SERVER["HTTP_USER_AGENT"], "MSIE")) {
  header("ETag: MSIE");
} else {
  header("ETag: notMSIE");
}
?>

如果組件必須通過最新修改日期之外的一些東西來進行驗證,則ETag是一種強大的方法。

如果無需自定義ETag,最好簡單地將其移除。Apache和IIS都將ETag視爲一個性能問題,並建議修改ETag的內容。可以移除inode值。然而,剩下的(大小,時間戳)是重複信息(Last-Modified和Content-Length頭可以提供完全等價的詳細),所以最好將ETag移除(還可以降低響應頭大小)。

在Apache中,只需向Apache配置文件中簡單地添加一行配置就可以移除ETag:

FileETag none

規則14—-使Ajax可緩存

異步與及時

Ajax是異步的,但並代表Ajax就是及時的。這取決於Ajax請求時被動的還是主動的。被動請求是爲了將來使用而預先發起的。例如,在一個基於web的email客戶端中,可能會使用被動請求在用戶真正需要之前下載用戶的地址簿。經過被動加載,當用戶需要爲一個Email消息添加地址時,客戶端能確保地址簿已經存在於緩存中。主動請求是基於用戶當前的操作而發起的。例如查找所有與用戶搜索條件相匹配的Email消息。

對於主動請求的Ajax,即使是異步的,用戶仍然可能需要等待相應。

現實中的Ajax緩存

其實Ajax緩存就是HTTP緩存。
以下HTTP響應頭是可以用來做Ajax緩存的:

  • Expires: 應該被應用在你知道內容何時被修改的情況下。 例如,如果是股票價格您可能會設置一個在10秒後過期的數值。對於照片,你可以設置一個更長時間的Expires頭,因爲你指望它永遠不改變。Expires頭允許瀏覽器在一段時間內可以重複使用緩存內容,並避免任何不需要的同服務器的交互過程.

  • Last-Modified: 設置這個標記會通知瀏覽器可以使用If-Modified-Since頭來產生一個條件GET請求以便檢查其本地緩存。如果數據不需要更新,服務器將使用HTTP 304狀態碼來響應此請求

  • Cache-Control: 如果允許,這應該被設置爲’public’,使其他用戶可以在中間代理和緩存服務器上存儲和共享數據,在Firefox上,這還將啓用針對HTTPS的緩存

這裏有一篇文章介紹Ajax緩存

總結

關於這本書的進階指南已經出來了。。有時間有機會再寫吧。要找實習了。 2017.2.9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章