關於大型網站技術演進的思考(二十一):網站靜態化處理—Web前端優化(下)(13)

發表於2015-03-09

本篇繼續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後面我們會專門加上一個版本號引數,例如:

我們只要改變12345這個引數的值就能讓瀏覽器重新從服務端獲取靜態資源,這個時候問題來了,如果外部css檔案或javascript檔案被很多頁面引用,那麼我們就不得不去手動的更改頁面裡引用這些外部檔案的版本號,這個操作難免會有遺漏,就算遺漏問題好解決,如果我們的頁面是使用服務端模板開發的,那麼就可能導致生產釋出時候重啟生產伺服器,為了靜態資源釋出重啟伺服器的確讓人感覺有點得不償失。那麼我們又如何來解決這個問題呢?

我們分析下這個問題的本質就是頁面引用外部css檔案和javascript檔案的行為其實包含一個動態性,那麼我們要解決這個問題就是要拆分出這個動態性,也就是把要變化的版本號這個動態性拆分出來進行單獨處理,一般我們就會通過模板語言來重新編寫link和script標籤的程式碼,例如在jsp技術裡我們可以自定義一個標籤,將版本號作為引數傳入標籤裡,當專案釋出時候,模板引擎會根據版本引數不同重新編譯出link和script標籤,但是這個做法還是有問題的,例如jsp頁面技術,要想更改版本號就得重啟服務,所以這個時候我們把版本號的計算功能做到獨立的快取裡,當檔案改變後我們通過更改配置重新整理快取,這樣就可以不用重啟伺服器就能重新整理靜態資源的版本號了。如果我們網站使用了網站靜態化處理,那麼我們可以把這個操作遷移到反向代理這邊來做,把該操作作為動靜整合的一部分,如果我們使用了ESI技術,那麼無非就是重新整理下ESI對應的快取即可。這個動態重新整理靜態資源版本號的操作在網際網路裡已經很流行了,但是現在大部分技術都是關注在如何檢測靜態檔案是否發生變化上,例如使用md5技術計算檔案的md5值啊,或者是修改下檔案的名字啊,但是這些手段使用時候都沒考慮到是否重啟伺服器的問題,最終導致設計方案使用起來比較麻煩,我覺得如何檢測檔案是否變化很重要,如果方案能實現在檢測變化的基礎上做到不用重啟伺服器就能重新整理快取,這樣才能讓該方案更加完整和實用。

OK了,終於把網站靜態系列寫完了,後面我將開啟一個新的系列那就是分散式和SOA,本來我想把分散式和SOA分成兩個系列,最近覺得這兩個系列合併在一起比較好,不過寫新系列前可能需要一段時間準備,最近一直寫部落格都沒抽出時間好好看書,應該要花點時間看書好好學習下了。

今天週五了,我是歌手馬上要開始,要準備看電視了,最後還是按照慣例祝大家晚安,生活愉快啦。

相關文章