如何讓你的網頁開啟速度降低到1s內
經過一段時間的專案實踐,在先前方案的基礎上又做了很多深入的優化工作。最終將好奇心日報的網頁開啟速度將降低到了1s內,Web端和Mobile端載入3屏資料消耗的流量也大幅降低。
模擬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優化
6、等等...
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方案更好。但是它只適用於純色圖示。推薦使用 阿里巴巴向量圖示庫
2、用base64減少不必要的網路請求
base64碼相容性
上文提到的sprite圖和iconfont字型檔案,對於有些場景並不適合,比如:
1、小背景圖,無法放到精靈圖中,通常迴圈平鋪小塊來設定大背景。
2、小gif圖,無法放到精靈圖中,發請求又太浪費。
base64使用場景
注意:cssnano壓縮css的時候,對於部分規則的base64 uri不能識別,會出現誤傷,如下圖,cssnano壓縮的時候會將//
壓縮為/
:
cssnano壓縮base64
原因是:cssnano會跳過data:image
/data:application
後面的字串,但是不會跳過data:img
,所以如果你使用的工具生成的是data:img
,建議換工具或者直接將其修改為data:image
。
3、圖片延遲載入
圖片是網頁中流量佔比最多的部分,也是需要重點優化的部分。
圖片延遲載入的原理就是先不設定img的src屬性,等合適的時機(比如滾動、滑動、出現在視窗內等)再把圖片真實url放到img的src屬性上。更多內容請移步上一篇博文:
圖片延遲載入方案
固定寬高值的圖片
固定寬高值的圖片延遲載入比較簡單,因為寬高值都可以設定在css中,只需考慮src的替換問題,推薦使用lazysizes。
// 引入js檔案
<script src="lazysizes.min.js" async=""></script>
// 非響應式 例子
<img src="" data-src="image.jpg" class="lazyload" />
// 響應式 例子,自動計算合適的圖片
<img
data-sizes="auto"
data-src="image2.jpg"
data-srcset="image1.jpg 300w,
image2.jpg 600w,
image3.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:image/gif;base64,R0lGODlhAQABAAA
AACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-src="真實url"
alt="<%= article.title %>">
此外,為了讓效果更佳,尤其是文章詳情頁中的大圖,我們可以加上loading效果。
.article-detail-bd {
.lazyload {
opacity: 0;
}
.lazyloading {
opacity: 1;
background: #f7f7f7 url(/images/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解決這個問題。
4、JS/CSS按需打包
按需打包是webpack獨特的優勢,如果有需要通過此種方式來管理模組之間的依賴關係,強烈推薦引入!webpack門檻較高,可以看看我之前的部落格:
webpack 入門
webpack 模組化機制
好奇心日報是典型的多頁應用,為了快取通用程式碼,我們使用webpack按需打包的同時,還利用webpack的CommonsChunkPlugin 外掛
抽離出公用的JS/CSS程式碼,便於快取,在請求數量和公用程式碼的快取之間做了一個很好的平衡。
5、延遲載入ga統計
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、JS/CSS/HTML壓縮
這也是常規手段,就不介紹太多,主要的方式有:
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外掛 來實現條件編譯。
條件編譯
2、gzip壓縮
gzip壓縮也是比較常規的優化手段。前端並不需要做什麼實際的工作,後臺配置下伺服器就行,效果非常明顯。如果你發現你的網站還沒有配置gzip,那麼趕緊行動起來吧。
gzip壓縮原理
如果瀏覽器支援gzip壓縮,在傳送請求的時候,請求頭中會帶有Accept-Encoding:gzip
。然後伺服器會將原始的response
進行gzip壓縮,並將gzip壓縮後的response
傳輸到瀏覽器,緊接著瀏覽器進行gzip解壓縮,並最終反饋到網頁上。
支援gzip壓縮的請求頭
gzip壓縮效果
那麼gzip壓縮的效果有多明顯呢?保守估計,在已經完成JS/CSS/HTML壓縮的基礎上,還能降低60-80%左右的大小。
gzip壓縮效果
但需要注意,gzip壓縮會消耗伺服器的效能,不能過度壓縮。
所以推薦只對JS/CSS/HTML等資源做gzip壓縮。圖片的話,託管到第三方的圖片建議開啟gzip壓縮,託管到自己應用伺服器的圖片不建議開啟gzip壓縮。
3、JS/CSS按需載入
和前面提到的按需打包不同。JS/CSS按需打包
是預編譯發生的事情,保證只打包當前頁面相關的邏輯。JS/CSS按需載入
是執行時發生的事情,保證只加載當前頁面第一時間使用到的邏輯。
那麼怎麼實現按需載入呢?好奇心日報使用webpack提供的require
及require.ensure
方法來實現按需載入,值得一提的是,除了指定的按需載入檔案列表,webpack還會自動解析回撥函式的依賴及指定列表的深層次依賴,並最終打包成一個檔案。
webpack按需載入
上訴程式碼的實現效果是:只有當點選登入按鈕的時候,才會去載入登入相關的JS/CSS資源。資源在載入成功後自動執行。
4、圖片壓縮,jpg優化
託管到應用伺服器的圖片壓縮
可以手動處理,也可以通過gulp子任務來處理。
手動處理的話,推薦一個網站 tinypng ,雖然是有失真壓縮,但壓縮效果極好。
gulp子任務處理的話,推薦使用gulp-imagemin外掛,自動化處理,效果也還不錯。
// 圖片壓縮
gulp.task('images', function() {
return gulp.src(config.src)
.pipe(plumber(handleErrors))
.pipe(newer(config.dest))
.pipe(logger({ showChange: true }))
.pipe(imagemin()) // 壓縮
.pipe(gulp.dest(config.dest));
});
託管到第三方平臺的圖片壓縮
比如七牛雲平臺,他們會有一套專門的方案來對圖片壓縮,格式轉換,裁剪等。只需要在url後面加上對應的引數即可,雖然偶爾會有一些小bug,但整體來說,託管方案比用自家應用伺服器方案更優。
改變引數,實現不同程度的壓縮
jpg優化
除了對圖片進行壓縮之外,對透明圖床沒有要求的場景,強烈建議將png轉換為jpg,效果很明顯!
如下圖,將png格式化為jpg格式,圖片相差差不多8倍!
png轉jpg,體積相差八倍
再次強調,可以轉換成jpg的圖片,強烈建議轉換成jpg!
5、webp優化 & srcset優化
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:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoB\
AAEAAwA0JaQAA3AA/vuUAAA=',
webpanimationSrc = 'data:image/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAA\
SAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAA\
AAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA';
webpTest(webpSrc, 'webp');
webpTest(webpanimationSrc, 'webpanimation');
})();
借鑑modernizr,實現了檢測webp/webpanimation相容性的函式,從程式碼中可以看出,檢測原理就是模擬下載對應格式的圖片,在非同步函式中可以得到相容性結果。
接下來就是替換url為webp格式
// 獲取webp格式的src
function _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相容性
srcset相容性
如上所述,在對webp優化的時候,我們順道模擬實現了srcset:根據螢幕畫素密度來設定最適合的圖片寬高。
lazysizes原本提供了srcset選項,也可以借用lazysizes的方案來實現srcset,有興趣的可以去看看原始碼。
又到總結的時候了?
本部落格圍繞好奇心日報的具體實踐,在優化頁面載入速度方面的做了一系列思考。整體來說,涉及的知識面比較廣:包括webpack & gulp的構建系統、圖片的webp優化、伺服器的gzip配置、瀏覽器的載入順序、圖片延遲載入方案等等。
文中提到的gulp子任務,後續也會有一系列好奇心日報專案的相關實踐,會覆蓋gulp子任務的設計思路,構建系統的架構,以及具體子任務的剖析和講解,敬請關注。
如果該博文對你有一些幫助,請點選喜歡支援一下,也歡迎在評論區留下你的建議和討論。
文/齊修_qixiuss(簡書作者)
原文連結:http://www.jianshu.com/p/d857c3ff78d6