還記得圖檔延遲加載方案 那篇博文嗎?當初分析了 定寬高值 定寬高比
和
這兩種常見的圖檔延遲加載場景,也介紹了他們的應對方案,還做了一點技術選型的工作。
經過一段時間的項目實踐,在先前方案的基礎上又做了很多深入的優化工作。最終将好奇心日報的網頁打開速度将降低到了1s内,Web端和Mobile端加載3屏資料消耗的流量也大幅降低。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIiV2dhcmbw5iNxZWStFmYvwVbvNmLs92bjlWd05iMn1Wavw1LcpDc0RHaiojIsJye.png!web)
模拟WIFI條件下的網頁加載
該篇文章結合具體的項目實踐,将圍繞如何更快的通路網頁展開,細化到具體的技術方案,以及實踐中可能遇到的坑,希望對大家有一定的啟發和幫助。
為什麼要優化網頁加載速度?
好奇心日報無論是設計還是内容都追求高品質,于是豐富的圖文混合成了标配:首頁的banner圖,文章詳情頁的配圖,研究所有趣的gif圖等等。
特别嚴重的時候,一篇文章有十多個gif圖,加載花費的時間10-20秒之長,加載消耗的流量幾十M之多,嚴重影響了使用者體驗!尤其是Mobile端,一寸流量一寸金;3-5s打不開頁面,使用者都會直接逃離。是以網頁加載速度優化勢在必行!
我們都知道一個網頁的加載流程大緻如下:
1、解析HTML結構。
2、加載外部腳本和樣式表檔案。
3、解析并執行腳本代碼。// 部分腳本會阻塞頁面的加載
4、DOM樹建構完成。//DOMContentLoaded 事件
5、加載圖檔等外部檔案。
6、頁面加載完畢。//load 事件
一句話就是:請求HTML,然後順帶将HTML依賴的JS/CSS/iconfont等其他資源一并請求過來。
那麼優化網頁的加載速度,最本質的方式就是:減少請求數量 與 減小請求大小。
減少請求數量
1、将小圖示合并成sprite圖或者iconfont字型檔案
2、用base64減少不必要的網絡請求
3、圖檔延遲加載
4、JS/CSS按需打包
5、延遲加載ga統計
6、等等...
減小請求大小
1、JS/CSS/HTML壓縮
2、gzip壓縮
3、JS/CSS按需加載
4、圖檔壓縮,jpg優化
5、webp優化 & srcset優化
JS/CSS按需打包
JS/CSS按需加載
是兩個不同的概念:
JS/CSS按需打包
是預編譯發生的事情,保證隻打包目前頁面相關的邏輯。
JS/CSS按需加載
是運作時發生的事情,保證隻加載目前頁面第一時間使用到的邏輯。
接下來我們将結合兩個本質的優化方式介紹具體的實踐方法。
如何減少請求數量?
1、合并圖示,減少網絡請求
合并圖示是減少網絡請求的常見的優化手段,網頁中的小圖示特征是體積小、數量多,而浏覽器同時發起的并行請求數量又是有限制的,是以這些小圖示會嚴重的影響網頁的加載速度,阻礙關鍵内容的請求和呈現
sprite圖
合并sprite圖是慢工細活兒,并沒有特别大的技術含量,但卻是每個前端開發都必須掌握的技術。
剛入門的前端直接手動切圖拼圖即可。
經驗豐富的前端可以嘗試利用建構工具實作自動化,推薦使用。 gulp.spritesmith插件 。
// 建構視圖檔案gulp.task('sprites', function() { var spriteData = gulp.src(config.src)
.pipe(plumber(handleErrors))
.pipe(newer(config.imgDest))
.pipe(logger({ showChange: true }))
.pipe(spritesmith({
cssName: 'sprites.css',
imgName: 'sprites.png',
cssTemplate: path.resolve('./gulp/lib/template.css.handlebars')
})); var imgStream = spriteData.img
.pipe(buffer())
.pipe(gulp.dest(config.imgDest)); var cssStream = spriteData.css
.pipe(gulp.dest(config.cssDest)); return merge([imgStream, cssStream]);
});
sprite圖不适合無線端的響應式場景,是以越來越作為一個備用方案。
iconfont字型檔案
iconfont字型檔案是用字型編碼的形式來實作圖示效果,既然是文字,那就可以随意設定顔色設定大小,相對來說比sprite方案更好。但是它隻适用于純×××标。推薦使用阿裡巴巴矢量圖示庫
base64碼相容性
上文提到的sprite圖和iconfont字型檔案,對于有些場景并不适合,比如:
1、小背景圖,無法放到精靈圖中,通常循環平鋪小塊來設定大背景。
2、小gif圖,無法放到精靈圖中,發請求又太浪費。
base64使用場景
注意:cssnano壓縮css的時候,對于部分規則的base64 uri不能識别,會出現誤傷,如下圖,cssnano壓縮的時候會将
//
壓縮為
/
:
cssnano壓縮base64
原因是:cssnano會跳過
data:p_w_picpath
/
data:application
後面的字元串,但是不會跳過
data:img
,是以如果你使用的工具生成的是
data:img
,建議換工具或者直接将其修改為
data:p_w_picpath
。
圖檔是網頁中流量占比最多的部分,也是需要重點優化的部分。
圖檔延遲加載的原理就是先不設定img的src屬性,等合适的時機(比如滾動、滑動、出現在視窗内等)再把圖檔真實url放到img的src屬性上。更多内容請移步上一篇博文:圖檔延遲加載方案
固定寬高值的圖檔
固定寬高值的圖檔延遲加載比較簡單,因為寬高值都可以設定在css中,隻需考慮src的替換問題,推薦使用 lazysizes 。
// 引入js檔案<script src="lazysizes.min.js" async=""></script>// 非響應式 例子<img src="" data-src="p_w_picpath.jpg" class="lazyload" />// 響應式 例子,自動計算合适的圖檔<img data-sizes="auto"
data-src="p_w_picpath2.jpg"
data-srcset="p_w_picpath1.jpg 300w,
p_w_picpath2.jpg 600w,
p_w_picpath3.jpg 900w" class="lazyload" />// iframe 例子<iframe frameborder="0"
class="lazyload"
allowfullscreen=""
data-src="//www.youtube.com/embed/ZfV-aYdU4uE"></iframe>
注意:浏覽器解析img标簽的時候,如果src屬性為空,浏覽器會認為這個圖檔是壞掉的圖,會顯示出圖檔的邊框,影響市容。
第一塊是初始狀态,第四塊是成功狀态,第二塊第三塊是影響市容的狀态
lazysizes延遲加載過程中會改變圖檔的class:預設lazyload,加載中lazyloading,加載結束:lazyloaded。結合這個特性我們有兩種解決上述問題辦法:
1、設定opacity:0,然後在顯示的時候設定opacity:1。
// 漸現 lazyload.lazyload,
.lazyloading{
opacity: 0;
}
.lazyloaded{
opacity: 1;
transition: opacity 500ms; //加上transition就可以實作漸現的效果}
2、用一張預設的圖占位,比如1x1的透明圖或者灰圖。
<img class="lazyload"
src="data:p_w_picpath/gif;base64,R0lGODlhAQABAAA
AACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-src="真實url"
alt="<%= article.title %>">
此外,為了讓效果更佳,尤其是文章詳情頁中的大圖,我們可以加上loading效果。
.article-detail-bd {
.lazyload {
opacity: 0;
}
.lazyloading {
opacity: 1;
background: #f7f7f7 url(/p_w_picpaths/loading.gif) no-repeat center;
}
}
固定寬高比的圖檔
固定寬高比的圖檔延遲加載相對來說複雜很多,比如文章詳情頁的圖檔,由于裝置的寬度值不确定,是以高度值也不确定,這時候工作的重心反倒放到了如何确定圖檔的高度上。
為什麼要确定圖檔的高度呢?因為單個圖檔的加載是從上往下,是以會導緻頁面抖動,不僅使用者體驗很差,而且對于性能消耗很大,因為每次抖動都會觸發reflow(重繪)事件,之前的博文 網站性能優化 之 渲染性能 也分析過重繪對于性能的消耗問題。
固定寬高比的圖檔抖動問題,有下列兩種主流的方式可以解決:
1、第一種方案使用padding-top或者padding-bottom來實作固定寬高比。優點是純CSS方案,缺點是HTML備援,并且對輸出到第三方不友好。
<div style="padding-top:75%">
<img data-src="" alt="" class="lazyload"><div>
2、第二種方案在頁面初始化階段利用ratio設定實際寬高值,優點是html幹淨,對輸出到第三方友好,缺點是依賴js,理論上會至少抖動一次。
<img data-src="" alt="" class="lazyload" data-ratio="0.75">
那麼,這個
padding-top: 75%;
data-ratio="0.75"
的資料從哪兒來呢?在你上傳圖檔的時候,需要背景給你傳回原始寬高值,計算得到寬高比,然後儲存到data-ratio上。
好奇心日報采用的第二種方案,主要在于第一種方案對第三方輸出不友好:需要對img設定額外的樣式,但第三方平台通常不允許引入外部樣式。
确定第二種方案之後,我們定義了一個設定圖檔高度的函數:
// 重置圖檔高度,僅限文章詳情頁function resetImgHeight(els, placeholder) {
var ratio = 0,
i, len, width; for (i = 0, len = els.length; i < len; i++) {
els[i].src = placeholder;
width = els[i].clientWidth; //一定要使用clientWidth
if (els[i].attributes['data-ratio']) {
ratio = els[i].attributes['data-ratio'].value || 0;
ratio = parseFloat(ratio);
} if (ratio) {
els[i].style.height = (width * ratio) + 'px';
}
}
}
我們将以上代碼的定義和調用都直接放到了HTML中,就為了一個目的,第一時間計算圖檔的高度值,降低使用者感覺到頁面抖動的可能性,保證最佳效果。
// 原生代碼<img alt=""
data-ratio="0.562500"
data-format="jpeg"
class="lazyload"
data-src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
src="">// 解析之後的代碼<img alt=""
data-ratio="0.562500"
data-format="jpeg"
class="lazyloaded"
data-src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
src="http://img.qdaily.com/uploads/20160807124000WFJNyGam85slTC4H.jpg"
style="height: 323.438px;">
我們不僅儲存了寬高比,還儲存了圖檔格式,是為了後期可以對gif做進一步的優化。
注意事項
1、避免圖檔過早加載,把臨界值調低一點。在實際項目中,并不需要過早就把圖檔請求過來,尤其是Mobile項目,過早請求不僅浪費流量,也會因為請求太多,導緻頁面加載速度變慢。
2、為了最好的防抖效果,設定圖檔高度的JS代碼内嵌到HTML中以便第一時間執行。
3、根據圖檔寬度設定高度時,使用clientWidth而不是width。這是因為Safari中,第一時間執行的JS代碼擷取圖檔的width失敗,是以使用clientWidth解決這個問題。
按需打包是webpack獨特的優勢,如果有需要通過此種方式來管理子產品之間的依賴關系,強烈推薦引入!webpack門檻較高,可以看看我之前的部落格:
webpack 入門
webpack 子產品化機制
好奇心日報是典型的多頁應用,為了緩存通用代碼,我們使用webpack按需打包的同時,還利用webpack的
CommonsChunkPlugin 插件
抽離出公用的JS/CSS代碼,便于緩存,在請求數量和公用代碼的緩存之間做了一個很好的平衡。
async & defer屬性
html5中給script标簽引入了async和defer屬性。
帶有async屬性的script标簽,會在浏覽器解析時立即下載下傳腳本同時不阻塞後續的document渲染和script加載等事件,進而實作腳本的異步加載。
帶有defer屬性的script标簽,和async擁有類似的功能。并且他們有可以附帶一個onload事件
<script src="" defer onload="init()">
async和defer的差別在于:async屬性會在腳本下載下傳完成後無序立即執行,defer屬性會在腳本下載下傳完成後按照document結構順序執行。
由于defer和async的相容性問題,我們通常使用
動态建立script标簽
的方式來實作異步加載腳本,即
document.write('<script src="" async></script>');
,該方式也可以避免阻塞。
ga統計代碼
ga統計代碼采用就是
動态建立script标簽
方案。
該方法不阻塞頁面渲染,不阻塞後續請求,但會阻塞window.onload事件,頁面的表現方式是進度條一直加載或loading菊花一直轉。
是以我們延遲執行ga初始化代碼,将其放到window.onload函數中去執行,可以防止ga腳本阻塞window.onload事件。進而讓使用者感受到更快的加載速度。
将ga加載綁定到onload事件上
如何減小請求大小?
這也是正常手段,就不介紹太多,主要的方式有:
1、通過建構工具實作,比如webpack/gulp/fis/grunt等。
2、背景預編譯。
3、利用第三方online平台,手動上傳壓縮。
無論是第二種還是第三種方式,都有其局限性,第一種方法是目前的主流方式,憑借良好的插件生态,可以實作豐富的建構任務。
在好奇心日報的項目中,我們使用webpack & gulp作為建構系統的基礎。
簡單介紹一下JS/CSS/HTML壓縮方式和一些注意事項
JS壓縮
JS壓縮
:使用webpack的
UglifyJsPlugin
插件,同時做一些代碼檢測。
new webpack.optimize.UglifyJsPlugin({ mangle: {
except: ['$super', '$', 'exports', 'require']
}})
CSS壓縮
CSS壓縮
:使用cssnano壓縮,同時使用postcss做一些自動化操作,比如自動加字首、屬性fallback支援、文法檢測等。
var postcss = [
cssnano({
autoprefixer: false,
reduceIdents: false,
zindex: false,
discardUnused: false,
mergeIdents: false
}),
autoprefixer({ browers: ['last 2 versions', 'ie >= 9', '> 5% in CN'] }),
will_change,
color_rgba_fallback,
opacity,
pseudoelements,
sorting
];
HTML壓縮
HTML壓縮
:使用htmlmin壓縮HTML,同時對不規範的HTML寫法糾正。
// 建構視圖檔案-build版本gulp.task('build:views', ['clean:views'], function() { return streamqueue({ objectMode: true },
gulp.src(config.commonSrc, { base: 'src' }),
gulp.src(config.layoutsSrc, { base: 'src' }),
gulp.src(config.pagesSrc, { base: 'src/pages' }),
gulp.src(config.componentsSrc, { base: 'src' })
)
.pipe(plumber(handleErrors))
.pipe(logger({ showChange: true }))
.pipe(preprocess({ context: { PROJECT: project } }))
.pipe(gulpif(function(file) { if (file.path.indexOf('.html') != -1) { return true;
} else { return false;
}
}, htmlmin({
removeComments: true,
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true,
ignoreCustomFragments: [/<%[\s\S]*?%>/,
/<\?[\s\S]*?\?>/,
/<meta[\s\S]*?name="viewport"[\s\S]*?>/]
})))
.pipe(gulp.dest(config.dest));
});
某個第三方平台要求
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0, user-scalable=no">
必須寫成小數點格式,而htmlmin預設會将小數格式化為整數,是以額外添加了排除項:
/<meta[\s\S]*?name="viewport"[\s\S]*?>/
。到現在都沒懂這個第三方平台咋想的!
條件編譯
由于好奇心日報項目較多,我們費了很大的心思抽離出前端項目,實作了前後分離。但有些場景下,我們為了将相關代碼維護在一個檔案中,同時又針對不同項目執行不同的邏輯,這時候,強烈推薦使用 gulp-preprocess插件 來實作條件編譯。
gzip壓縮也是比較正常的優化手段。前端并不需要做什麼實際的工作,背景配置下伺服器就行,效果非常明顯。如果你發現你的網站還沒有配置gzip,那麼趕緊行動起來吧。
gzip壓縮原理
如果浏覽器支援gzip壓縮,在發送請求的時候,請求頭中會帶有
Accept-Encoding:gzip
。然後伺服器會将
原始的response
進行gzip壓縮,并将
gzip壓縮後的response
傳輸到浏覽器,緊接着浏覽器進行gzip解壓縮,并最終回報到網頁上。
支援gzip壓縮的請求頭
gzip壓縮效果
那麼gzip壓縮的效果有多明顯呢?保守估計,在已經完成JS/CSS/HTML壓縮的基礎上,還能降低60-80%左右的大小。
但需要注意,gzip壓縮會消耗伺服器的性能,不能過度壓縮。
是以推薦隻對JS/CSS/HTML等資源做gzip壓縮。圖檔的話,托管到第三方的圖檔建議開啟gzip壓縮,托管到自己應用伺服器的圖檔不建議開啟gzip壓縮。
和前面提到的按需打包不同。
JS/CSS按需打包
JS/CSS按需加載
那麼怎麼實作按需加載呢?好奇心日報使用webpack提供的
require
及
require.ensure
方法來實作按需加載,值得一提的是,除了指定的按需加載檔案清單,webpack還會自動解析回調函數的依賴及指定清單的深層次依賴,并最終打包成一個檔案。
webpack按需加載
上訴代碼的實作效果是:隻有當點選登入按鈕的時候,才會去加載登入相關的JS/CSS資源。資源在加載成功後自動執行。
托管到應用伺服器的圖檔壓縮
可以手動處理,也可以通過gulp子任務來處理。
手動處理的話,推薦一個網站 tinypng ,雖然是有損壓縮,但壓縮效果極好。
gulp子任務處理的話,推薦使用 gulp-p_w_picpathmin插件 ,自動化處理,效果也還不錯。
// 圖檔壓縮gulp.task('p_w_picpaths', function() { return gulp.src(config.src)
.pipe(plumber(handleErrors))
.pipe(newer(config.dest))
.pipe(logger({ showChange: true }))
.pipe(p_w_picpathmin()) // 壓縮
.pipe(gulp.dest(config.dest));
});
托管到第三方平台的圖檔壓縮
比如七牛雲平台,他們會有一套專門的方案來對圖檔壓縮,格式轉換,裁剪等。隻需要在url後面加上對應的參數即可,雖然偶爾會有一些小bug,但整體來說,托管方案比用自家應用伺服器方案更優。
改變參數,實作不同程度的壓縮
jpg優化
除了對圖檔進行壓縮之外,對透明圖床沒有要求的場景,強烈建議将png轉換為jpg,效果很明顯!
如下圖,将png格式化為jpg格式,圖檔相差差不多8倍!
png轉jpg,體積相差八倍
再次強調,可以轉換成jpg的圖檔,強烈建議轉換成jpg!
webp優化
粗略看一眼,卧槽,相容性這麼差,也就安卓浏覽器及chrome浏覽器對它的支援還算給力。
webp相容性
另一方面,webp優化能在jpg的基礎上再降低近50%的大小。其優化效果明顯。此外,如果浏覽器支援webpanimation,還能對gif做壓縮!
普通圖檔webp優化
gif圖檔優化
相容性差,但效果好!最終好奇心決定嘗試一下。
1、判斷浏覽器對webp及webpanimation的相容性。
2、如果浏覽器支援webp及webpanimation,将其替換成webp格式的圖檔。
鑒于浏覽器對webp的支援比較局限,我們采用漸進更新的方式來優化:對于不支援webp的浏覽器,不做處理;對于支援webp的浏覽器,将圖檔src替換成webp格式。
那麼如何判斷webp相容性呢?
// 檢測浏覽器是否支援webp// 之是以沒寫成回調,是因為即使isSupportWebp=false也無大礙,但卻可以讓代碼更容易維護(function() { function webpTest(src, name) {
var img = new Image(),
isSupport = false,
className, cls;
img.onload = function() {
isSupport = !!(img.height > 0 && img.width > 0);
cls = isSupport ? (' ' + name) : (' no-' + name);
className = document.querySelector('html').className
className += cls;
document.querySelector('html').className = className.trim();
};
img.onerror = function() {
cls = (' no-' + name);
className = document.querySelector('html').className
className += cls;
document.querySelector('html').className = className.trim();
};
img.src = src;
} var webpSrc = 'data:p_w_picpath/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoB\
AAEAAwA0JaQAA3AA/vuUAAA=',
webpanimationSrc = 'data:p_w_picpath/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAA\
SAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAA\
AAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA';
webpTest(webpSrc, 'webp');
webpTest(webpanimationSrc, 'webpanimation');
})();
借鑒modernizr,實作了檢測webp/webpanimation相容性的函數,從代碼中可以看出,檢測原理就是模拟下載下傳對應格式的圖檔,在異步函數中可以得到相容性結果。
接下來就是替換url為webp格式
// 擷取webp格式的srcfunction _getWebpSrc(src) {
var dpr = Math.round(window.devicePixelRatio || 1),
ratio = [1, 1, 1.5, 2, 2, 2],
elHtml = document.querySelector('html'),
isSupportWebp = (/(^|\s)webp(\s|$)/i).test(elHtml.className),
isSupportWebpAnimation = (/(^|\s)webpanimation(\s|$)/i).test(elHtml.className),
deviceWidth = elHtml.clientWidth,
isQiniuSrc = /img\.qdaily\.com\//.test(src),
format = _getFormat(src),
isGifWebp, isNotGifWebp, regDetailImg; if (!src || !isQiniuSrc || !format || format == 'webp') { return src;
}
isNotGifWebp = (format != 'gif' && isSupportWebp);
isGifWebp = (format == 'gif' && isSupportWebpAnimation); // 根據螢幕分辨率計算大小
src = src.replace(/\/(thumbnail|crop)\/.*?(\d+)x(\d+)[^\/]*\//ig, function(match, p0, p1, p2) { if(dpr > 1){
p1 = Math.round(p1 * ratio[dpr]);
p2 = Math.round(p2 * ratio[dpr]);
match = match.replace(/\d+x\d+/, p1 + 'x' + p2)
} return match;
}); if(isNotGifWebp || isGifWebp) { // 替換webp格式,首頁/清單頁
src = src.replace(/\/format\/([^\/]*)/ig, function(match, p1) { return '/format/webp';
});
}
}
1、window的螢幕像素密度不一定是整數,mac浏覽器縮放之後,螢幕像素密度也不是整數。是以擷取dpr一定要取整:
dpr = Math.round(window.devicePixelRatio || 1);
2、
ratio = [1, 1, 1.5, 2, 2, 2]
表示:1倍屏使用1倍圖,2倍屏使用1.5倍圖,3倍屏以上都用2倍圖。這兒的規則可以按實際情況來設定。
3、webp優化更适合托管到第三方的圖檔,簡單修改參數就可以擷取不同的圖檔。
devicePixelRatio相容性
srcset相容性
如上所述,在對webp優化的時候,我們順道模拟實作了srcset:根據螢幕像素密度來設定最适合的圖檔寬高。
lazysizes原本提供了srcset選項,也可以借用lazysizes的方案來實作srcset,有興趣的可以去看看源碼。
又到總結的時候了?
本部落格圍繞好奇心日報的具體實踐,在優化頁面加載速度方面的做了一系列思考。整體來說,涉及的知識面比較廣:包括webpack & gulp的建構系統、圖檔的webp優化、伺服器的gzip配置、浏覽器的加載順序、圖檔延遲加載方案等等。