如何讓你的網頁打開速度下降到 1s 內

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

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

模擬WIFI條件下的網頁加載html

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

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

好奇心日報不管是設計仍是內容都追求高品質,因而豐富的圖文混合成了標配:首頁的banner圖,文章詳情頁的配圖,研究全部趣的gif圖等等。html5

特別嚴重的時候,一篇文章有十多個gif圖,加載花費的時間10-20秒之長,加載消耗的流量幾十M之多,嚴重影響了用戶體驗!尤爲是Mobile端,一寸流量一寸金;3-5s打不開頁面,用戶都會直接逃離。因此網頁加載速度優化勢在必行!webpack

咱們都知道一個網頁的加載流程大體以下:git

一、解析HTML結構。github

二、加載外部腳本和樣式表文件。web

三、解析並執行腳本代碼。// 部分腳本會阻塞頁面的加載chrome

四、DOM樹構建完成。//DOMContentLoaded 事件gulp

五、加載圖片等外部文件。

六、頁面加載完畢。//load 事件

一句話就是:請求HTML,而後順帶將HTML依賴的JS/CSS/iconfont等其餘資源一併請求過來。

那麼優化網頁的加載速度,最本質的方式就是:減小請求數量 與 減少請求大小。

減小請求數量

一、將小圖標合併成sprite圖或者iconfont字體文件

二、用base64減小沒必要要的網絡請求

三、圖片延遲加載

四、JS/CSS按需打包

五、延遲加載ga統計

六、等等...

減少請求大小

一、JS/CSS/HTML壓縮

二、gzip壓縮

三、JS/CSS按需加載

四、圖片壓縮,jpg優化

五、webp優化 & srcset優化

六、等等...

JS/CSS按需打包 和 JS/CSS按需加載 是兩個不一樣的概念:

JS/CSS按需打包 是預編譯發生的事情,保證只打包當前頁面相關的邏輯。

JS/CSS按需加載 是運行時發生的事情,保證只加載當前頁面第一時間使用到的邏輯。

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

如何減小請求數量?

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

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

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提供的 require 及 require.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格式的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'; }); }}

注意事項

一、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子任務的設計思路,構建系統的架構,以及具體子任務的剖析和講解,敬請關注。

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

相關文章
相關標籤/搜索