0.1秒的價值!淺談Web前端頁面提速問題
記得面試現(xiàn)在這份工作的時候,一位領(lǐng)導(dǎo)語重心長地談道——當今的世界是互聯(lián)網(wǎng)的世界,IT企業(yè)之間的競爭是很激烈的,如果一個網(wǎng)頁的加載和顯示速度,相比別人的站點頁面有那么0.1秒的提升,那也是很大的一個成就。
然后我不知道怎么寫下去了,就在群里問了那群狗頭軍師,結(jié)果是這樣的。。。

好的,是時候“語鋒一轉(zhuǎn)”切回主題了 —— 如何提升我們站點頁面的訪問速度、減少等待時間,從而最大化地提升用戶訪問體驗?zāi)兀?/p>
針對這個問題,我們今天會從前端的角度來提出系列解決方案,它們都能有效地提升你頁面的訪問速度。
一. 減少對服務(wù)器的文件請求
常規(guī)的HTTP請求屬于“請求”-“應(yīng)答”-“斷開”形式的短連接,每一個獨立的資源我們都會向服務(wù)器發(fā)去一份get請求,再等服務(wù)端將我們需要的文件傳回來。每一次資源的請求都實實在在地耗費了一次“連接-等待-接收”的時間(當然將http請求設(shè)為keep-alive長連接狀態(tài)可以減少“連接”的次數(shù)和時間),如果我們能有效減少對服務(wù)器文件的請求次數(shù),便意味著我們可以從這塊省下一些頁面等待時間,也可以順便減少服務(wù)器的負擔(dān)。
對于這個解決方案,我們可以這么做:
1. 使用css sprite技術(shù)合并多個圖片為單個圖片文件,實際使用時通過background-position來定位背景位置(相信大家第一個想到的也是這個吧);
2. 合并多個css樣式文件為單個樣式文件,合并多個腳本為單個腳本,再在頁面中引用合并后的樣式/腳本文件。對于這個你可以使用r.js來幫忙,但我個人倒是不怎么推薦這個方法,因為合并了文件之后,多個頁面之間公共部分的樣式/腳本文件就無法緩存到客戶端了;
3. 使用base64編碼來展示圖片。就如圖github 404頁面那樣:

你可以使用這個工具來幫你把圖片轉(zhuǎn)換為base64編碼的文件流,但常規(guī)只推薦你把這種方式使用在用戶重復(fù)訪問量較少的頁面,因為它們雖然無須從服務(wù)端get一遍,但也無法緩存在客戶端,導(dǎo)致用戶每次訪問頁面都要重新渲染一次。而且冗長的文件流代碼會占用你頁面很大的代碼空間,維護起頁面來估計也會挺心塞;
4. 將小塊的css、js代碼段直接寫在頁面上,而非在頁面引入獨立的樣式/腳本文件。相信有的朋友看慣了“保持結(jié)構(gòu) (標記)、表現(xiàn) (樣式)、行為 (腳本)三者分離”的規(guī)范,對此觀點可能有些意見。只能說規(guī)范不是教條,適合自己的才是硬道理。直接把小段的、復(fù)用率低的樣式/腳本直接寫于頁面上帶來的利還是大于弊的(弊可能也就是增大了頁面代碼量、不那么好維護了點)。反觀所有主流門戶網(wǎng)站的頁面源文件,基本沒有一個是把樣式/腳本都全部作為外部文件引入的(無論他們是否從減少服務(wù)器請求這點出發(fā),事實都是這樣);
5. 利用http-equiv="expires"元標簽,設(shè)定一個未來的某時間點作為頁面文件過期時間,用戶在過期時間之前所獲取到的頁面文件都僅從緩存中去取。不過這個辦法太死板(有時候即使服務(wù)端及時把過期時間更改為已結(jié)束時間,客戶端可能都不會按照新更改的規(guī)則去服務(wù)端獲取新文件資源),常規(guī)是不推薦使用的。
二. 減少文件大小
文件太大(特別是圖片)導(dǎo)致加載時間較長,往往都是影響頁面加載體驗的頭號大敵,那么盡可能減少請求文件的大小便是相當重要的事情了,我們可以做的事情有:
1. 壓縮樣式/腳本文件,就此你可以使用gulp或者grunt來實現(xiàn)這點,它們均能很好地減少css/js文件的大?。▽τ趈s還能起到混淆變量、函數(shù)名的作用);
2. 針對性選擇圖片格式,在無透明背景需求下,對于顏色較單一、無色彩漸變的圖片僅使用gif格式,對于jpg圖片也可按照其清晰度要求,在導(dǎo)出jpg的時候選擇對應(yīng)的“品質(zhì)”進行優(yōu)化:

如果你喜歡嘗鮮,可以學(xué)淘寶那樣使用webp圖片格式,它能很好地優(yōu)化同畫質(zhì)下的文件大小:

3. 使用Font Awesome來替代頁面上的圖標,其原理是使用@font-face讓用戶下載一個非常小的UI字體包,把頁面上用到的圖標以字符的形式來顯示,從而減少了圖片需求和圖標文件大小。
三. 適度使用CDN
使用CDN有幾個好處:如果用戶在其它站點下載過這個CDN資源,那么來我們站點僅僅從緩存獲取即可;減少了對自己站點服務(wù)器的文件請求(外部CDN的情況下),減少服務(wù)器負擔(dān);多個域會使瀏覽器允許異步下載資源的最大數(shù)量增多,比如一個站點只從一個域來請求資源,那么FireFox只允許同時刻最多異步下載2個文件,但如果使用了外部CDN來引入資源,那么FF允許在同時異步下載本域中的兩個資源外,還額外允許同時異步下載另一個域(CDN)下的2個資源。
但是使用CDN有一個很大的問題——增加了dns解析的開銷,如果一個頁面同時引入了多個CDN的資源,可能會因為dns解析而陷入較多的等待時間,導(dǎo)致得不償失。
對于這個問題,常規(guī)是建議一個站點下只使用同一個可靠、快速的CDN來引入各種所需資源即可,也就是說,建議一個頁面從2個不同的域(比如站點域和CDN域)下來請求資源是最佳的選擇(據(jù)說這個結(jié)論是雅虎前端工程師提出的)。
四. 延遲請求、異步加載腳本
在各主流瀏覽器下,常規(guī)情況,我們的腳本文件跟隨其它資源文件一樣都是異步下載的,但這里存在一個問題——比如FireFox下載好腳本后的一小段時間內(nèi)會有“執(zhí)行阻塞”的情況發(fā)生,也就是說瀏覽器下載好腳本后執(zhí)行它的這段時間里,瀏覽器的其它行為被阻塞,導(dǎo)致頁面上的其它資源都是無法被請求和下載的:

如果你頁面里存在js代碼執(zhí)行時間過長的情況,那么用戶就會明顯感覺到頁面的延遲。解決這個問題有一個簡單的方法——將腳本請求標簽放置到</body>結(jié)束標簽前,使得頁面上的腳本成為最后被請求的資源,自然也不會阻塞其它頁面資源的請求事件了。
另外,雖然上面提到“我們的腳本文件跟隨其它資源文件一樣都是異步下載的”,但異步下載不代表異步執(zhí)行,為了嚴格保證腳本邏輯順序和依賴關(guān)系的正確性,瀏覽器會按照腳本被請求的先后順序來執(zhí)行腳本。那么問題就來了——如果頁面上的腳本依賴關(guān)系并不大,甚至沒有任何相互間的依賴,那么瀏覽器的這套規(guī)則就僅僅增加了頁面請求阻塞時間而已(就像你花大錢買了一筆保險,但被保險期間你平安無事啥都沒發(fā)生…… 嗯,這個比喻有點反人類……)。
解決這個問題的辦法無非就是讓腳本無阻塞地異步執(zhí)行,比如給script標簽加上defer和async屬性或者動態(tài)注入腳本(可以參考這里),但這些都不是良好的解決方案,要么存在兼容性問題,要么太麻煩還無法處理依賴。
個人是推薦使用 requireJS(AMD規(guī)范) 或 seaJS(CMD規(guī)范) 來異步加載腳本并處理模塊依賴的,前者將“依賴前置”(預(yù)加載所有被依賴腳本模塊,執(zhí)行速度最快),后者走的“依賴就近”(懶加載被依賴腳本模塊,請求腳本更科學(xué)),你可以根據(jù)項目具體需求來選擇最合適的。
五. 延遲請求首屏外的文件
先解釋下,“首屏”指的是頁面初始化時候的頁面內(nèi)容顯示區(qū)域,也就是頁面一加載,用戶就首先看到的區(qū)域。
比如像京東啊淘寶啊,對于需要滾動頁面才能看到的圖片內(nèi)容,都做了類似lazyload的處理,這些無非都是走了代理模式的理念,但的確給用戶一個錯覺——這個頁面更快地加載完了,因為我很快就看到了屏幕上的內(nèi)容(即使我還沒下拉滾動條,而頁面后方的文件其實還沒真正加載呢)。
我們可以這樣實現(xiàn)此方案,不依賴任何lazyload庫,拿圖片來做示范,我們可以這樣編寫首屏外的圖片(假設(shè)某張圖片地址是a.jpg)的img標簽:
<img src="data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="a.jpg">
如上所示,頁面初步加載這張圖片的時候是直接以base64的方式(當然你也可以統(tǒng)一使用一張占位圖loading.gif來替代)來快速顯示一張極小的圖片的,而圖片本身的真實路徑是存在data-src屬性內(nèi)的,我們可以在頁面加載結(jié)束后再向服務(wù)器請求它真實的文件并替換:
function init(){
var imgDefer = document.getElementsByTagName('img');
for (var i=0; i<imgDefer.length; i++) {
if(imgDefer[i].getAttribute('data-src')) {
imgDefer[i].setAttribute('src',imgDefer[i].getAttribute('data-src'));
}
}
}
window.onload = init;
如上是對圖片的延遲加載處理,對于視頻、音頻文件,可以采取完全一樣的原理來延遲加載,從而有效減少頁面初始化等待時間。
六. 優(yōu)化頁面模塊排放順序
這里有一個很好的例子,比如有一個頁面是這樣的——左邊是側(cè)邊欄,用于存放用戶的頭像啊、資料啊,以及網(wǎng)站投放的廣告啊,而右側(cè)是文章內(nèi)容區(qū)域:

那么我們的代碼很可能是這樣的:
<body>
<sidebar>
<!-- 側(cè)邊欄內(nèi)容 --></sidebar>
<content>
<!-- 文章內(nèi)容 -->
</content>
</body>
于是乎,瀏覽器按照它的UI單線程準則從上到下先加載了側(cè)邊欄,再加載我們的文章。。。
很明顯,這樣不是一個人性化的加載順序,我們得弄清楚,頁面上各個區(qū)域模塊,對于用戶而言,哪個才是最重要、最應(yīng)當首先展示的。
對于上面的例子,文章內(nèi)容才應(yīng)該是用戶首先要看到、需要瀏覽器優(yōu)先請求和顯示的區(qū)域。所以我們得修改我們的代碼為:
<body>
<content>
<!-- 文章內(nèi)容 -->
</content>
<sidebar>
<!-- 側(cè)邊欄內(nèi)容 -->
</sidebar>
</body>
當然這里僅僅是用一個小示例來挑起各位的腦洞,懂得舉一反三和實際運用才是硬道理。
七. 其它建議
1. 不要在css中使用@import,它會讓一個樣式文件去等待另一個樣式文件的請求,無形中增加了頁面等待時間(當然如果走的scss,@import就是另一回事了,呃跑題了~);
2. 避免頁面或者頁面文件重定向查找,這相當于你走進了一間衛(wèi)生間,然后看到上面的牌子說“此處不同,請去前面左拐的衛(wèi)生間”,又得重走一遍;
3. 減少無效請求——比如通過css/js來請求一個不存在的資源,可能會導(dǎo)致較長的等待和阻塞(直到它返回錯誤信息);
4. 無論你是否決定將腳本放到頁尾,但一定要保障腳本放置于樣式文件后方;
5. 配置.htaccess文件、走Gzip頁面壓縮形式、開啟keep-alive連接模式等后端解決方案,這邊就不細說了。
相信頁面提速的方案絕對不僅僅局限于上面提到的這些,而且每個項目都有著各種需求和情況,只能按需選擇適合自己的方案。
但最重要的還是——盡量把用戶的體驗放在第一位,無論是頁面的加載或者交互,都應(yīng)當多從用戶角度切入去思考和設(shè)計最優(yōu)選的方案,這樣相信你一定能做出出色、受歡迎的作品。
今天聊的就是這些,共勉~
相關(guān)文章

15 個為編程初學(xué)者準備的網(wǎng)站(都是國外的一些網(wǎng)站)
今天的文章,我們將分享15個可以學(xué)習(xí)編程的網(wǎng)站,這些網(wǎng)站上提供了很多編程教程,圖書以及編程練習(xí),希望對你有用2024-11-02- 這篇文章主要介紹了web開發(fā)中的長度單位主要包括px,pt,em等,需要的朋友可以參考下2023-08-06
網(wǎng)頁前端開發(fā)的一些尺寸單位(px,rem單位)
px單位是絕對單位,一般用于pc端網(wǎng)頁開發(fā),因為是絕對單位所以在移動端上的使用體驗并不是很好,rem它是描述相對于當前根元素字體尺寸,是相對單位,它可以根據(jù)根元素的變換而2023-08-06WEB前端優(yōu)化必備js/css壓縮工具YUI-compressor詳解與集成用法
壓縮工具層次不窮,各有優(yōu)點,選擇適合的壓縮工具為將來做項目開發(fā)使用是一件很重要的事情??!在這介紹YUI-compressor,需要的朋友可以參考下2023-06-21- 瀏覽器是多進程的,有瀏覽器主進程,網(wǎng)絡(luò)進程,渲染進程,插件進程等,在將html,css,javascript解析成一個頁面的時候,就需要多個進程的分工合作2023-05-01
- 本文為大家整理了常用的文件對應(yīng)的MIME類型,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-25
postman中form-data、x-www-form-urlencoded、raw、binary的區(qū)別介紹
這篇文章介紹了postman中form-data、x-www-form-urlencoded、raw、binary的區(qū)別,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-28網(wǎng)頁中使用Unicode字符的介紹(&#,\u等)
國際組織制定了可以容納世界上所有文字和符號的字符編碼方案,稱為Unicode,是通用字符集Universal Character Set的縮寫,用以滿足跨語言、跨平臺進行文本轉(zhuǎn)換、處理的要求2021-11-27前端實現(xiàn)字符串GBK與GB2312的編解碼(小結(jié))
這篇文章主要介紹了前端實現(xiàn)字符串GBK與GB2312的編解碼(小結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2020-12-02
這篇文章主要介紹了告別硬編碼讓你的前端表格自動計算,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-27



