本篇繼續web前端優化的討論,開始我先講個我所知道的一個故事,有家大型的企業順應時代發展的潮流開始投身於互聯網行業了,它們為此專門設立了一個事業部,不過該企業把這個事業部里的人事成本,系統運維成本特別是硬件采購的成本都由總公司來承擔,當然互聯網業務上的市場營銷成本這塊還是由該事業部自己承擔,可是網站一年運維下來,該公司發現該事業部里最大的成本居然不是市場營銷的開銷,而是短信業務和寬帶使用上的開銷,是不是有點讓人感到意外呢?下面我來分析下這個場景吧。
短信這塊是和通訊運營商有關,很難從根本上解決,當然該企業可以考慮使用像微信這樣的工具來分攤下短信的成本,但是寬帶流量消耗這個問題卻很難有第二選擇了,可能有人會感到詫異,一家做互聯網的企業,用戶都是使用自己掏錢的寬帶來上網的,為啥企業會有寬帶流量的成本呢?其實互聯網公司的后台服務都是會放置在IDC即數據中心里的,除非你的企業是真正的高富帥,或者你本身的核心業務就是互聯網業務,這樣的企業才有可能會自建數據中心,絕大部分企業都會租用第三方的數據中心,而且有些企業為了容災還會在不同地域建立不同的數據中心,不同數據中心之間是通過專線來通訊的,而專線的成本是很高的,我們想讓自己開發的網站讓更多人用,可以通過改造服務端並發處理能力來達到這個目的,但是這里還有一個制約因素,那就是服務端使用的帶寬,一般而言,企業選擇多大帶寬是可以估算出來,最終采用一個合理的帶寬,但是,如果這家公司是電商類型網站,就很有可能碰到像雙十一啊,或者自身做大促銷的情況,這個時候服務端的負載壓力就會成倍增加,遠遠超出平時的網絡流量,如是企業會提前擴充帶寬,而擴充的帶寬流量是昂貴的,這樣就會無形增加網站運營成本。如果我們不去思考成本問題,當今社會講求環保,例如淘寶就說它們網站沒完成一次交易使用的電量可以煮熟兩個雞蛋,它們網站一天下來消耗的電量相當於中國一個三線城市一天消耗的電量,那么如果我們能節約每次請求消耗的寬帶流量其實也就是在節約能源,所以不管是從成本角度還是從環保角度提高寬帶的利用率都是有很大的現實意義的。
Web前端優化里有一個技巧就是壓縮http請求的數據量,這個技巧很多人都是簡單認為http請求的數據越小,那么http處理速度就更快些,不過我認為這結論其實是一個相對的結論,現在的網速是越來越快,很多人家里接入的寬帶已經使用上了光纖,50兆,百兆的寬帶已經飛進了尋常百姓家了,那么這時候其實網絡傳輸100kb數據和傳輸300kb數據的效率差異基本可以忽略不計了,當然並非每個人網絡訪問速度都這么快,例如我們使用手機的2G網絡上網,那么100kb和300kb的傳輸效率還是會有很大差異的,所以壓縮http請求大小這個手段在客戶端這塊是一種解決短板的技巧,這個短板就是照顧那些上網速度太慢的人了,而非對人人平等的技術手段,但是這個問題換到服務端就不同了,減少http報文的數據大小可以提升企業對寬帶的利用率,是一種節約網站運營成本的一個重要手段,因此壓縮http傳輸數據的大小是一個很有價值的技術手段。
用來壓縮http請求數據大小的手段很多,例如使用Gzip壓縮http請求,壓縮圖片等等,不過我這里要特別說明一個手段那就是減少cookie存儲數據的大小,這是一個常常被忽視的壓縮http請求大小的技術手段。不過cookie技術對很多初學者常常會感到差異,cookie是客戶端的數據,為什么服務端和客戶端都能操作它,難道服務端也會存儲一份cookie的備份嗎?之所以初學者會對cookie使用有疑問,這主要是初學者不太清楚cookie信息除了保存在瀏覽器端,它還會包含在http報文頭里的,每個http請求響應都會帶着cookie信息進行傳遞,所以cookie既可以被客戶端操作也能被服務端操作,如果我們忽視cookie這個特點,再加上我們濫用cookie,最后cookie被撐滿了,這也就意味每次請求響應的數據量會增加,而這些信息可能大部分都不會被使用,純粹多余。而網站在開發和維護時候很容易不自覺的讓cookie變得越來越多,越來越大,如果我們一開始就明確cookie這個特點,提前設計cookie使用規范,那么就可以一定程度上規避cookie不合理使用導致的http數據量變大的問題。如果網站使用了單獨的靜態資源服務器,並且把靜態資源放置在單獨的域名下面,這個時候我們還要避免給靜態資源域名下使用cookie技術,因為靜態資源基本都不會有狀態信息,使用cookie只會無謂的增加請求的數據大小。
網絡是存儲設備里效率最差的,如果頁面加載時候還有些請求是一個壞請求,例如頁面訪問的某些靜態資源突然丟了,瀏覽器這個時候會有一個容錯的做法,這個做法具體是:瀏覽器不能確定有問題的請求到底是因為網速慢了還是找不到,所以瀏覽器會多次請求這個url,直到瀏覽器認為這個url的確是有問題無法訪問了,瀏覽器才不去繼續請求了,如果碰到的資源正好是外部javascript文件,那就很有可能阻塞整個頁面的加載,所以剔除頁面里的壞請求也是要經常留心的事情。
我們如果再進一步分析下web前端優化的一些手段,就會發現很多優化手段其實都是基於靜態資源來處理的,靜態資源的特點就是在一定時間范圍內不會發生變化的,而且當用戶請求靜態資源時候,服務端不需要任何計算操作即消耗CPU資源就能把結果返回給客戶端,靜態資源這種不參與計算的特點就可以讓靜態資源和業務應用服務器解耦,因此我們可以把靜態資源單獨抽取出來放置在CDN或者是請求效率處理更佳的靜態資源服務器上。和靜態資源相對的動態資源就很難做到這點,我們仔細回味下網站后台整個應用架構,就會發現所有網站都會使用存儲系統即基本都會用數據庫,而且應用服務器和數據庫又是一種緊耦合的關系,因為我們想消除存儲系統的狀態問題基本是不可能完成的任務,這就讓應用服務器沒法做成CDN的形式,因此動態資源處理想使用CDN這種減少距離對網絡通訊影響的手段基本是很麻煩的。我覺得網站靜態化處理其實是根據web前端優化技術產生的技術,它讓網站靜態化資源和動態資源分離做的更好,所以我說網站靜態化技術是充分發揮web前端優化手段的重要保證,這也就是我為什么會把web前端優化的內容也要放在網站靜態化處理系列里的原因了。
靜態資源因為在一定時間里不會發生變化,容易被緩存,而且瀏覽器本身也有緩存機制,那么如果我們把靜態資源緩存在瀏覽器端,用戶請求網站就不需要再去請求網絡資源,這個效率不就更高了嗎?現實情況的確是如此,但是我們想讓瀏覽器端充分發揮這個緩存作用其實並非那么簡單,因為我們會碰到如下的問題,具體如下:
問題一:網站對瀏覽器的控制是一種被動控制,用戶才是控制瀏覽器的主動方,用戶的很多行為都會導致網站對瀏覽器的緩存設計策略失效,如果緩存失效,那么用戶再去訪問網站時候就得重新請求資源,所以為了彌補瀏覽器緩存的不可靠性,CDN技術以及靜態資源服務器的使用就派上用場了。
問題二:瀏覽器緩存網頁部分資源可以讓網頁加載的更快,但是要做到這一點之前,我們首先要明確何時采用,同時采用何種方式讓客戶端緩存這些可以被緩存的資源?那么我們在知道某個用戶要訪問網站了,我們提前把需要緩存的資源發送個用戶,讓用戶先緩存下這些資源,這個做法肯定是開國際玩笑了,一般我們都是在用戶第一次訪問網站時候將可以緩存的資源緩存起來,這個時候問題又來了,那就是用戶第一次訪問網站時候因為需要緩存的資源都沒有被緩存,所以全部的資源都要通過網絡請求下載,這個時候就會導致用戶第一次訪問網站頁面的效率很差,有人可能認為網站又不是設計為訪問一次的產品,只要資源被緩存了網頁就會更快的,要是用戶覺得第一次訪問慢了,就先忍忍吧,以后會快的,這個想法又是再開國際玩笑了。就算用戶忍受了第一次訪問慢的情形,但是如果用戶使用這個網站的時間間隔是很長的,例如某些專業性的網站,它的用戶可能會很長一段時間后再訪問該網站,而過了這段時間后,瀏覽器緩存的資源很有可能失效了,這個時候用戶再去訪問又等於是第一次訪問了,那么我們這個緩存設計方案基本就是無效了。
問題二所反映的問題也就表明我們在如何合理使用瀏覽器緩存這塊上是需要考慮用戶的使用場景的,需要加入一些業務性的策略了,只有這樣瀏覽器緩存方案才能充分發揮其優勢。下面我就來談論下瀏覽器端緩存策略設計的問題了。
首先我們來看一個場景,用戶第一次訪問網站,訪問的是網站的首頁,我們按照web前端優化原則設計了網站首頁,特別是使用了一個優化原則就是把css合並成一個外部css文件,把javascript代碼也合並成一個外部文件,首頁都引入了這兩個外部文件,這種情況首頁訪問至少會產生三個http請求,可是網站首頁其實沒有那么復雜,也就是說首頁使用的css代碼和javascript代碼其實並不太多,如果我們把這些代碼就放置到頁面內部,那么首頁加載就只有一個請求,雖然這會導致這個請求的數據量變大,不過按照我前面說到壓縮http請求數據大小,其實在提升網絡傳輸速度上這個角度是值得商榷的,但是多個http請求就會導致瀏覽器打開更多連接,而每個連接的建立和銷毀卻是十分消耗計算資源的,那么如果我們能把三個請求合並成一個請求完成就一定會讓請求處理的更快,可是這個做法就會導致css和javascript文件沒法被緩存,那么以后想復用它們就麻煩了。碰到這樣的問題我們又該如何來抉擇了?最理想的結果就是二者兼顧,但是要兼顧二者,那么頁面就一定要處理這三個http請求了,我們到底能不能做到二者兼顧了?答案是肯定的,我們可以做到的。我們仔細的分析下這個場景,就會發現,快速加載頁面和緩存靜態資源在頁面首次訪問這個背景下其實是兩個不同的業務操作,用戶第一次訪問首頁用戶只會關心頁面是否快速被加載,至於加載靜態資源的行為以及緩存靜態資源的行為,用戶是不用關心,因此我們就可以拆分這兩個操作,首先是讓頁面快速被加載,等頁面加載完畢后,我們在通過異步手段來加載外部的靜態資源,這樣就可以做到二者兼顧了,至於如何異步加載靜態資源,我在以前的文章里講述過,這篇文章就是《探真無阻塞加載javascript腳本技術,我們會發現很多意想不到的秘密》,不了解這個技術的朋友可以看看本篇文章。
不過要讓上面的方案發揮作用是有一個大的前置條件的,那就是我們要判斷出用戶到底是不是第一次訪問,而且因為外部的css文件和外部的javascript文件都被我們合並成了一個文件,這也就是說首頁里內嵌的css代碼和javascript代碼和外部文件是有一個冗余的,如果用戶第二次訪問時候不需要這些操作了,那么讓首頁保持這個冗余是不是就沒有這個必要了?特別是javascript代碼,重復的javascript代碼總是讓人覺得不放心。這兩個問題的核心還是在於如何判斷用戶是否第一次訪問,判斷用戶的行為那就是屬於判斷用戶狀態的問題了,用戶的狀態標記在服務端使用的是session技術,瀏覽器端使用的是cookie技術,而session技術是一個臨時會話存儲技術,因此使用session是沒法判斷用戶以前是否訪問過該網站,所以這里只能使用cookie技術(如果瀏覽器支持html5,客戶端保存用戶狀態的信息手段就更加多了,不一定非要使用cookie了),也就是當用戶第一次訪問網站時候,我們將一些可以標記用戶是否訪問過網站的狀態信息存儲在cookie里,那么用戶再次訪問這個網站時候,http請求就會把cookie信息傳送給服務端,服務端通過cookie信息判定用戶是否第一次訪問,這個時候服務端可以剔除頁面里內嵌的css代碼和javascript代碼,同時可以阻止瀏覽器再異步加載外部css文件和外部javascript文件行為,這樣用戶再次訪問網站的行為也不會被用戶第一次訪問行為干擾了。
上面場景里還有一個優化手段的使用是值得商榷的,那就是我們把網站所有的css代碼和javascript代碼合並到一個文件里。這里我首先來講講把所有javascript代碼合並成一個文件的問題,一個網站會包含很多不同頁面,不同的頁面因為業務場景的不同,就會導致每個頁面都有專屬的處理業務邏輯的javascript代碼,如果我們簡單的認為把javascript代碼放置到外部文件就會讓頁面加載的更快,那么當我們合並外部文件時候這些和頁面緊耦合的業務代碼也會合並到一個文件里,最后就會導致最終的外部javascript文件變得特別大,對於瀏覽器而言,javascript代碼過多也會影響到頁面的加載效率和javascript的執行效率,而且這個超大的外部javascript文件對於某一個功能頁面而言有太多冗余的代碼,所以我們簡單把全部外部javascript文件合並成一個外部javascript文件這個做法其實並不是太好,因此到底哪些javascript外部文件應該被合並這是有所選擇的。而且把某些業務相關的javascript代碼就寫在頁面,和頁面一起被下載可能比我們單獨使用外部文件的javascript效率更高,因為單獨的外部javascript文件會增加頁面http請求的個數,那么我們在開發時候那些javascript代碼需要內嵌入頁面,那些javascript代碼要把放在單獨外部文件里這也是我們要注意的策略問題。
我們毫無原則的把外部css文件和javascript文件合並成少量的外部文件還會影響到網站的運維和瀏覽器的緩存策略,特別是緩存策略的失效機制,例如網站某個頁面做了css代碼或者javascript代碼的修改,而這些代碼上生產時候要被合並到一個外部的css文件和javascript文件里,而這些外部文件又被很多網頁引用,那么我們就不得不讓很多無關的網頁也需要刷新瀏覽器緩存,如果這個修改是作用於公共代碼這個問題還好理解,要是這個代碼是用於營銷活動這個特定場景下,那么這樣的刷新緩存操作就會顯得非常沒有必要,如果有天營銷活動結束了,我們是不是還要再刷新下緩存,剔除多余的代碼呢?所以如何合並外部的css代碼和javascript代碼我們還是要應該根據業務場景進行合理的分組的。
Web前端的工作是十分繁重的,網站是和終端用戶打交道,這些終端用戶都是網站的需求方,而web前端是處理終端用戶需求的排頭兵,用戶那么多,需求那么多,所以網站的前端頁面會經常的被修改,修改的頁面就要重新發布生產,這個時候我們就要刷新瀏覽器的緩存了,那如何來刷新頁面的緩存了,方法其實很簡單就是改變頁面url的參數,一般網站的靜態資源的url后面我們會專門加上一個版本號參數,例如:
www.cnblogs.com/sharpxiajun/a.css?v=1234556
我們只要改變12345這個參數的值就能讓瀏覽器重新從服務端獲取靜態資源,這個時候問題來了,如果外部css文件或javascript文件被很多頁面引用,那么我們就不得不去手動的更改頁面里引用這些外部文件的版本號,這個操作難免會有遺漏,就算遺漏問題好解決,如果我們的頁面是使用服務端模板開發的,那么就可能導致生產發布時候重啟生產服務器,為了靜態資源發布重啟服務器的確讓人感覺有點得不償失。那么我們又如何來解決這個問題呢?
我們分析下這個問題的本質就是頁面引用外部css文件和javascript文件的行為其實包含一個動態性,那么我們要解決這個問題就是要拆分出這個動態性,也就是把要變化的版本號這個動態性拆分出來進行單獨處理,一般我們就會通過模板語言來重新編寫link和script標簽的代碼,例如在jsp技術里我們可以自定義一個標簽,將版本號作為參數傳入標簽里,當項目發布時候,模板引擎會根據版本參數不同重新編譯出link和script標簽,但是這個做法還是有問題的,例如jsp頁面技術,要想更改版本號就得重啟服務,所以這個時候我們把版本號的計算功能做到獨立的緩存里,當文件改變后我們通過更改配置刷新緩存,這樣就可以不用重啟服務器就能刷新靜態資源的版本號了。如果我們網站使用了網站靜態化處理,那么我們可以把這個操作遷移到反向代理這邊來做,把該操作作為動靜整合的一部分,如果我們使用了ESI技術,那么無非就是刷新下ESI對應的緩存即可。這個動態刷新靜態資源版本號的操作在互聯網里已經很流行了,但是現在大部分技術都是關注在如何檢測靜態文件是否發生變化上,例如使用md5技術計算文件的md5值啊,或者是修改下文件的名字啊,但是這些手段使用時候都沒考慮到是否重啟服務器的問題,最終導致設計方案使用起來比較麻煩,我覺得如何檢測文件是否變化很重要,如果方案能實現在檢測變化的基礎上做到不用重啟服務器就能刷新緩存,這樣才能讓該方案更加完整和實用。
OK了,終於把網站靜態系列寫完了,后面我將開啟一個新的系列那就是分布式和SOA,本來我想把分布式和SOA分成兩個系列,最近覺得這兩個系列合並在一起比較好,不過寫新系列前可能需要一段時間准備,最近一直寫博客都沒抽出時間好好看書,應該要花點時間看書好好學習下了。
今天周五了,我是歌手馬上要開始,要准備看電視了,最后還是按照慣例祝大家晚安,生活愉快啦。