1. 程式人生 > >如何讓你的網頁開啟速度降低到1s內

如何讓你的網頁開啟速度降低到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提供的requirerequire.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