《轉》如何讓你的網頁加載時間下降到 1s 內

當初分析了定寬高值定寬高比這兩種常見的圖片延遲加載場景,也介紹了他們的應對方案,還作了一點技術選型的工做。javascript

通過一段時間的項目實踐,在先前方案的基礎上又作了不少深刻的優化工做。最終將好奇心日報的網頁打開速度將下降到了1s內,Web端和Mobile端加載3屏數據消耗的流量也大幅下降。php


模擬WIFI條件下的網頁加載

該篇文章結合具體的項目實踐,將圍繞如何更快的訪問網頁展開,細化到具體的技術方案,以及實踐中可能遇到的坑,但願對你們有必定的啓發和幫助。css

爲何要優化網頁加載速度?

好奇心日報不管是設計仍是內容都追求高品質,因而豐富的圖文混合成了標配:首頁的banner圖,文章詳情頁的配圖,研究全部趣的gif圖等等。
特別嚴重的時候,一篇文章有十多個gif圖,加載花費的時間10-20秒之長,加載消耗的流量幾十M之多,嚴重影響了用戶體驗!尤爲是Mobile端,一寸流量一寸金;3-5s打不開頁面,用戶都會直接逃離。因此網頁加載速度優化勢在必行!html

咱們都知道一個網頁的加載流程大體以下:
一、解析HTML結構。
二、加載外部腳本和樣式表文件。
三、解析並執行腳本代碼。// 部分腳本會阻塞頁面的加載
四、DOM樹構建完成。//DOMContentLoaded 事件
五、加載圖片等外部文件。
六、頁面加載完畢。//load 事件
一句話就是:請求HTML,而後順帶將HTML依賴的JS/CSS/iconfont等其餘資源一併請求過來。
那麼優化網頁的加載速度,最本質的方式就是:減小請求數量 與 減少請求大小。前端

減小請求數量

一、將小圖標合併成sprite圖或者iconfont字體文件
二、用base64減小沒必要要的網絡請求
三、圖片延遲加載
四、JS/CSS按需打包
五、延遲加載ga統計
六、等等...html5

減少請求大小

一、JS/CSS/HTML壓縮
二、gzip壓縮
三、JS/CSS按需加載
四、圖片壓縮,jpg優化
五、webp優化 & srcset優化
六、等等...java

JS/CSS按需打包JS/CSS按需加載是兩個不一樣的概念:
JS/CSS按需打包是預編譯發生的事情,保證只打包當前頁面相關的邏輯。
JS/CSS按需加載是運行時發生的事情,保證只加載當前頁面第一時間使用到的邏輯。webpack

接下來咱們將結合兩個本質的優化方式介紹具體的實踐方法。git

如何減小請求數量?

一、合併圖標,減小網絡請求

合併圖標是減小網絡請求的常見的優化手段,網頁中的小圖標特徵是體積小、數量多,而瀏覽器同時發起的並行請求數量又是有限制的,因此這些小圖標會嚴重的影響網頁的加載速度,阻礙關鍵內容的請求和呈現github

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減小沒必要要的網絡請求


base64碼兼容性

上文提到的sprite圖和iconfont字體文件,對於有些場景並不適合,好比:
一、小背景圖,沒法放到精靈圖中,一般循環平鋪小塊來設置大背景。
二、小gif圖,沒法放到精靈圖中,發請求又太浪費。


base64使用場景

注意:cssnano壓縮css的時候,對於部分規則的base64 uri不能識別,會出現誤傷,以下圖,cssnano壓縮的時候會將//壓縮爲/


cssnano壓縮base64

緣由是:cssnano會跳過data:image/data:application後面的字符串,可是不會跳過data:img,因此若是你使用的工具生成的是data:img,建議換工具或者直接將其修改成data:image

三、圖片延遲加載

圖片是網頁中流量佔比最多的部分,也是須要重點優化的部分。
圖片延遲加載的原理就是先不設置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。結合這個特性咱們有兩種解決上述問題辦法:
一、設置opacity:0,而後在顯示的時候設置opacity:1。

// 漸現 lazyload .lazyload, .lazyloading{ opacity: 0; } .lazyloaded{ opacity: 1; transition: opacity 500ms; //加上transition就能夠實現漸現的效果 }

二、用一張默認的圖佔位,好比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(重繪)事件,以前的博文 網站性能優化 之 渲染性能 也分析太重繪對於性能的消耗問題。

固定寬高比的圖片抖動問題,有下列兩種主流的方式能夠解決:
一、第一種方案使用padding-top或者padding-bottom來實現固定寬高比。優勢是純CSS方案,缺點是HTML冗餘,而且對輸出到第三方不友好。

<div style="padding-top:75%"> <img data-src="" alt="" class="lazyload"> <div>

二、第二種方案在頁面初始化階段利用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作進一步的優化。

注意事項

一、避免圖片過早加載,把臨界值調低一點。在實際項目中,並不須要過早就把圖片請求過來,尤爲是Mobile項目,過早請求不只浪費流量,也會由於請求太多,致使頁面加載速度變慢。
二、爲了最好的防抖效果,設置圖片高度的JS代碼內嵌到HTML中以便第一時間執行。
三、根據圖片寬度設置高度時,使用clientWidth而不是width。這是由於Safari中,第一時間執行的JS代碼獲取圖片的width失敗,因此使用clientWidth解決這個問題。

四、JS/CSS按需打包

按需打包是webpack獨特的優點,若是有須要經過此種方式來管理模塊之間的依賴關係,強烈推薦引入!webpack門檻較高,能夠看看我以前的博客:
webpack 入門
webpack 模塊化機制

好奇心日報是典型的多頁應用,爲了緩存通用代碼,咱們使用webpack按需打包的同時,還利用webpack的CommonsChunkPlugin 插件抽離出公用的JS/CSS代碼,便於緩存,在請求數量和公用代碼的緩存之間作了一個很好的平衡。

五、延遲加載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事件上

如何減少請求大小?

一、JS/CSS/HTML壓縮

這也是常規手段,就不介紹太多,主要的方式有:
一、經過構建工具實現,好比webpack/gulp/fis/grunt等。
二、後臺預編譯。
三、利用第三方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壓縮原理

若是瀏覽器支持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壓縮。

三、JS/CSS按需加載

和前面提到的按需打包不一樣。
JS/CSS按需打包是預編譯發生的事情,保證只打包當前頁面相關的邏輯。
JS/CSS按需加載是運行時發生的事情,保證只加載當前頁面第一時間使用到的邏輯。

那麼怎麼實現按需加載呢?好奇心日報使用webpack提供的requirerequire.ensure方法來實現按需加載,值得一提的是,除了指定的按需加載文件列表,webpack還會自動解析回調函數的依賴及指定列表的深層次依賴,並最終打包成一個文件。


webpack按需加載

上訴代碼的實現效果是:只有當點擊登陸按鈕的時候,纔會去加載登陸相關的JS/CSS資源。資源在加載成功後自動執行。

四、圖片壓縮,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!

五、webp優化 & srcset優化

webp優化

粗略看一眼,臥槽,兼容性這麼差,也就安卓瀏覽器及chrome瀏覽器對它的支持還算給力。


webp兼容性

另外一方面,webp優化能在jpg的基礎上再下降近50%的大小。其優化效果明顯。此外,若是瀏覽器支持webpanimation,還能對gif作壓縮!


普通圖片webp優化

gif圖片優化

兼容性差,但效果好!最終好奇心決定嘗試一下。
一、判斷瀏覽器對webp及webpanimation的兼容性。
二、若是瀏覽器支持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'; }); } }
注意事項

一、window的屏幕像素密度不必定是整數,mac瀏覽器縮放以後,屏幕像素密度也不是整數。因此獲取dpr必定要取整:dpr = Math.round(window.devicePixelRatio || 1);
二、ratio = [1, 1, 1.5, 2, 2, 2]表示:1倍屏使用1倍圖,2倍屏使用1.5倍圖,3倍屏以上都用2倍圖。這兒的規則能夠按實際狀況來設置。
三、webp優化更適合託管到第三方的圖片,簡單修改參數就能夠獲取不一樣的圖片。


devicePixelRatio兼容性
srcset兼容性

srcset兼容性

如上所述,在對webp優化的時候,咱們順道模擬實現了srcset:根據屏幕像素密度來設置最適合的圖片寬高。
lazysizes本來提供了srcset選項,也能夠借用lazysizes的方案來實現srcset,有興趣的能夠去看看源碼。

又到總結的時候了?

本博客圍繞好奇心日報的具體實踐,在優化頁面加載速度方面的作了一系列思考。總體來講,涉及的知識面比較廣:包括webpack & gulp的構建系統、圖片的webp優化、服務器的gzip配置、瀏覽器的加載順序、圖片延遲加載方案等等。

文中提到的gulp子任務,後續也會有一系列好奇心日報項目的相關實踐,會覆蓋gulp子任務的設計思路,構建系統的架構,以及具體子任務的剖析和講解,敬請關注。

若是該博文對你有一些幫助,請點擊喜歡支持一下,也歡迎在評論區留下你的建議和討論。



文/齊修_qixiuss(簡書做者) 原文連接:http://www.jianshu.com/p/d857c3ff78d6 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。
相關文章
相關標籤/搜索