前端從瀏覽器的渲染到性能優化

問題前瞻

1. 為什麼css需要放在頭部?

2. js為什麼要放在body後面?

3. 圖片的加載和渲染會阻塞頁面DOM構建嗎?

4. dom解析完才出現頁面嗎?

5. 首屏時間根據什麼來判定?

瀏覽器渲染

1.瀏覽器渲染圖解

瀏覽器渲染頁面主要經歷瞭下面的步驟:

1.處理 HTML 標記並構建 DOM 樹。

2.處理 CSS 標記並構建 CSSOM 樹。

3.將 DOM 與 CSSOM 合並成一個渲染樹。

4.根據渲染樹來佈局,以計算每個節點的幾何信息。

5.將各個節點繪制到屏幕上。

為構建渲染樹,瀏覽器大體上完成瞭下列工作:

從 DOM 樹的根節點開始遍歷每個可見節點。某些節點不可見(例如腳本標記、元標記等),因為它們不會體現在渲染輸出中,所以會被忽略。某些節點通過 CSS 隱藏,因此在渲染樹中也會被忽略,例如,上例中的 span 節點—不會出現在渲染樹中,—因為有一個顯式規則在該節點上設置瞭“display: none”屬性。對於每個可見節點,為其找到適配的 CSSOM 規則並應用它們。發射可見節點,連同其內容和計算的樣式

根據以上解析,DOM樹和CSSOM樹的構建對於頁面性能有非常大的影響,沒有DOM樹,頁面基本的標簽塊都沒有,沒有樣式,頁面也基本是空白的。所以具體css的解析規則是什麼?js是怎麼影響頁面渲染的?瞭解瞭這些,我們才能有的放矢,對頁面性能進行優化。

2.css解析規則

<div id="div1">    
    <div class="a">    
        <div class="b">    
            ...    
        </div>    
        <div class="c">    
            <div class="d">    
                ...    
            </div>    
            <div class="e">    
                ...    
            </div>    
        </div>    
    </div>    
    <div class="f">    
        <div class="c">    
            <div class="d">    
                ...    
            </div>    
        </div>    
    </div>    
</div>

#div1 .c .d {}    

.f .c .d {}    

.a .c .e {}    

#div1 .f {}    

.c .d{}

從左向右的匹配規則

從右向左的匹配規則

如果css從左向右解析,意味著我們需要遍歷更多的節點。不管樣式規則寫得多細致,每一個dom結點仍然需要遍歷,因為整個style rules還會有其它公共樣式影響。如果從右向左解析,因為子元素隻有一個父元素,所以能夠很快定位出當前dom符不符合樣式規則。

3.js加載和執行機制

首先明確一點,我們可以通過js去修改網頁的內容,樣式和交互等,這一意味著js會影響頁面的dom結構,如果js和dom構建並行執行,那麼很容易會出現沖突,所以js在執行時必然會阻塞dom和cssom的構建過程,不論是外部js還是內聯腳本。

js的位置是否影響dom解析?

首先我們為什麼提倡把js放在body標簽的後面去加載,因為從demo上看無論是放在head還是放在body後加載js,頁面domcontentload的時間都是一樣的:

我們從圖中可以看出js的加載和執行是阻塞dom解析的,但是因為頁面並不是一次就渲染完成,所以我們需要做的是盡量讓用戶看到首屏的部分被渲染出來,js放在頭部,則頁面的內容區域還沒有解析到就被阻塞瞭,導致用戶看到的是白屏,而js放在body後面,盡管此時頁面dom仍然沒有解析完成,但是已經渲染出一部分樓層瞭,這也是為什麼我們比較看重頁面的首屏時間。

隻有DOM和CSSOM樹構建好後並合並成渲染樹才能開始繪制頁面圖形,那是不是把整個DOM樹和CSSOM樹構建好後才能開始繪制頁面?這顯然是不符合我們平時訪問頁面的認知的,實際上:

為達到更好的用戶體驗,呈現引擎會力求盡快將內容顯示在屏幕上。它不必等到整個 HTML 文檔解析完畢之後,就會開始構建呈現樹和設置佈局。在不斷接收和處理來自網絡的其餘內容的同時,呈現引擎會將部分內容解析並顯示出來。

具體瀏覽器什麼時候進行首次繪制?可以查看本文對瀏覽器首次渲染時間點的探究。

4.圖片的加載和渲染機制

首先我們解答一下上面的問題:圖片的加載與渲染會不會阻塞頁面渲染?答案是圖片的加載和渲染不會影響頁面的渲染。

那麼標簽中的圖片和樣式中的圖片的加載和渲染時間是什麼樣的呢?

解析HTML【遇到標簽加載圖片】 —> 構建DOM樹加載樣式 —> 解析樣式【遇到背景圖片鏈接不加載】 —> 構建樣式規則樹加載javascript —> 執行javascript代碼把DOM樹和樣式規則樹匹配構建渲染樹【遍歷DOM樹時加載對應樣式規則上的背景圖片】計算元素位置進行佈局繪制【開始渲染圖片】

當然把DOM樹和樣式規則樹匹配構建渲染樹時,隻會把可見元素和它對應的樣式規則結合一起產出到渲染樹,這就意味有不可見元素,當匹配DOM樹和樣式規則樹時,若發現一個元素的對應的樣式規則上有display:none,瀏覽器會認為該元素是不可見的,因此不會把該元素產出到渲染樹上。

性能優化

css優化

1.盡量減少層級

#div p.class {    
    color: red;    
}    
    
.class {    
    color: red;    
}

層級減少,意味者匹配時遍歷的dom就少。
關於less嵌套的書寫規范也基於這個道理。

2.使用類選擇器而不是標簽選擇器

減少匹配次數

3.按需加載css

(function(){    
    window.gConfig =  window.gConfig || {};    
    window.gConfig.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);    
    var hClassName;    
    if(window.gConfig.isMobile){    
        hClassName = ' phone';    
    
        document.write('<link rel="stylesheet" href="https://res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/m/index.css" rel="external nofollow"  />');    
        document.write('<link rel="preload" href="//res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/m/index.js" rel="external nofollow"  crossorigin="anonymous" as="script" />');    
    
    }else{    
        hClassName = ' pc';    
    
        document.write('<link rel="stylesheet" href="https://res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/pc/index.css" rel="external nofollow"  />');    
        document.write('<link rel="preload" href="//res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/pc/index.js" rel="external nofollow"  crossorigin="anonymous" as="script" />');    
    
    }    
    var root = document.documentElement;    
    root.className += hClassName ;    
    
})();

async 與 defer

[來自https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html]

使用

  • 如果腳本是模塊化的並且不依賴於任何腳本,請使用async。
  • 如果該腳本依賴於另一個腳本或由另一個腳本所依賴,則使用defer。

減少資源請求

瀏覽器的並發數量有限,所以為瞭減少瀏覽器因為優先加載很多不必要資源,以及網絡請求和響應時間帶來的頁面渲染阻塞時間,我們首先應該想到的是減少頁面加載的資源,能夠盡量用壓縮合並,懶加載等方法減少頁面的資源請求。

延遲加載圖像

盡管圖片的加載和渲染不會影響頁面渲染,但是為瞭盡可能地優先展示首屏圖片和減少資源請求數量,我們需要對圖片做懶加載。

document.addEventListener("DOMContentLoaded", function() {    
    let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));    
    let active = false;    
    
    const lazyLoad = function() {    
        if (active === false) {    
            active = true;    
    
            setTimeout(function() {    
                lazyImages.forEach(function(lazyImage) {    
                    if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {    
                    lazyImage.src = lazyImage.dataset.src;    
                    lazyImage.srcset = lazyImage.dataset.srcset;    
                    lazyImage.classList.remove("lazy");    
    
                    lazyImages = lazyImages.filter(function(image) {    
                    return image !== lazyImage;    
                    });    
    
                    if (lazyImages.length === 0) {    
                    document.removeEventListener("scroll", lazyLoad);    
                    window.removeEventListener("resize", lazyLoad);    
                    window.removeEventListener("orientationchange", lazyLoad);    
                    }    
                }    
                });    
    
                active = false;    
            }, 200);    
        }    
    };    
    
    document.addEventListener("scroll", lazyLoad);    
    window.addEventListener("resize", lazyLoad);    
    window.addEventListener("orientationchange", lazyLoad);    
});

大促活動實踐

2.1 懶加載與異步加載

懶加載與異步加載是大促活動性能優化的主要手段,直白的說就是把用戶不需要或者不會立即看到的頁面數據與內容全都挪到頁面首屏渲染完成之後去加載,極限減小頁面首屏渲染的數據加載量與js,css執行帶來的性能損耗。

2.1.1 導航下拉的異步加載

導航的下拉內容是一塊結構非常復雜的html片段,如果直接加載,瀏覽器渲染的時間會拖慢頁面整體的加載時間:

所有我們需要通過異步加載方式來獲取這段html片段,等頁面首屏渲染結束後再添加到頁面上,大致的代碼如下:

$.ajax({    
    url: url, async: false, timeout: 10000,    
    success: function (data) {    
        container.innerHTML = data;    
        var appendHtml = $('<div class="footer-wrapper">' + container.querySelector('#footer').innerHTML + '</div>');    
        var tempHtml = '<div style="display:none;">' + '<script type="text/html" id="header-lazyload-html-drop" class="header-lazyload-html" data-holder="#holder-drop">' + appendHtml.find('#header-lazyload-html-drop').html() + '<\/script><script type="text/html" id="header-lazyload-html-mbnav" class="header-lazyload-html" data-holder="#holder-mbnav">' + appendHtml.find('#header-lazyload-html-mbnav').html() + '<\/script></div>';    
        $('#footer').append(tempHtml);    
            feloader.onLoad(function () {    
            feloader.use('@cloud/common-resource/header', function () {    
            });    
            $('#footer').css('display', 'block');    
        });    
    },    
    error: function (XMLHttpRequest, textStatus, errorThrown) {    
        console.log(XMLHttpRequest.status, XMLHttpRequest.readyState, textStatus);    
    },    
});

2.1.2 圖片懶加載

官網的cui套件中已經有lazyload的插件支持圖片懶加載,使用方法頁非常簡單:

官網的cui套件中已經有lazyload的插件支持圖片懶加載,使用方法頁非常簡單:
<divclass="list">
<imgclass="lazyload"data-src="http://www.placehold.it/375x200/eee/444/1"src="占位圖片URL"/>
<imgclass="lazyload"data-src="http://www.placehold.it/375x200/eee/444/2"src="占位圖片URL"/>
<imgclass="lazyload"data-src="http://www.placehold.it/375x200/eee/444/3"src="占位圖片URL"/>
<divclass="lazyload"data-src="http://www.placehold.it/375x200/eee/444/3"></div>
    ...
</div>

從代碼我們差不多可以猜出圖片懶加載的原理,其實就是我們通過覆蓋img標簽src屬性,使得img標簽開始加載時由於沒有src的具體圖片地址而不去加載圖片,等到重要資源加載完之後,通過監聽onload的時間或者滾動條的滾動時機再去重寫對應標簽的src值來達到圖片懶加載:

/**    
* load image    
* @param {HTMLElement} el - the image element    
* @private    
*/    
    _load(el) {    
        let source = el.getAttribute(ATTR_IMAGE_URL);    
        if (source) {    
            let processor = this._config.processor;    
            if (processor) {    
            source = processor(source, el);    
            }    
    
            el.addEventListener('load', () => {    
            el.classList.remove(CLASSNAME);    
        });    
        // 判斷是否是什麼元素    
            if (el.tagName === 'IMG') {    
                el.src = source;    
            } else {    
                // 判斷source是不是一個類名,如果是類名的話,則加到class裡面去    
                if (/^[A-Za-z0-9_-]+$/.test(source)) {    
                    el.classList.add(source);    
                 } else {    
                    let styles = el.getAttribute('style') || '';    
                    styles += `;background-image: url(${source});`;    
                    el.setAttribute('style', styles);    
                    el.style.backgroundImage = source; // = `background-image: url(${source});`;    
                }    
            }    
    
            el.removeAttribute(ATTR_IMAGE_URL);    
        }    
    }

具體的插件代碼大傢可以查看https://git.huawei.com/cnpm/lazyload。

同時官網的頁腳部分也采用瞭采用其它的加載方式也實現瞭懶加載的效果,頁腳的圖片都在css中引用,想要延遲加載頁腳圖片就需要延遲加載頁腳的css,但是延遲加載css造成的後果就是頁面加載的一瞬間頁腳會因為樣式確實而顯示錯亂,所以我們可以在css樣式加載前強勢隱藏掉頁腳部分,等css加載完成後,頁腳dom自帶的display:block會自動顯示頁腳。(==因為頁腳的seo特性沒有對其進行懶加載==)

2.1.3 樓層內容的懶加載

基於xtpl自帶的懶加載能力,配合pep定制頁面模板的邏輯,我們可以實現html的懶加載。在頁面初次渲染的時候,隻有每個樓層的大體框架和標題等關鍵信息,如果需要的話可以給默認圖片等占位,或設置最小高度占位,防止錨點定位失效。
當頁面滾動到該樓層的位置,js代碼方會執行,在初始化函數中,對該樓層的html進行加載,渲染,實現樓層圖片和html的懶加載,減少瞭首屏時間。
具體代碼如下:

<div class="nov-c6-cards j-content">    
</div>
public render(){    
    this.$el.find('.j-content').html(new Xtemplate(tpl).render(mockData))    
    ...    
}

2.1.4 套餐數據懶加載

套餐數據的加載一直以來都是令人頭疼的,本次雙十一對於套餐腳本也做瞭優化,不僅對數據進行瞭緩存,同時也可以在指定的范圍進行套餐數據的渲染——和上述所說的樓層懶加載配合,可以做到未展示的樓層,套餐數據不請求,下拉框不渲染,詢價接口不調用,在首屏不出現大量套餐的情況下,可以大大提升首屏加載的性能。

2.2.資源整合

2.2.1.頁頭頁尾資源統一維護

基礎模板的優化涉及到資源的合並,壓縮與異步加載,dom的延遲加載和圖片的懶加載。首先我們給出官網基礎模板引用的一部分js資源的表格:

這部分js存在問題是分散在pep的各個資產庫路徑維護,有些壓縮瞭,有些沒有壓縮,js的加載也基本是順序執行,所以我們對這個部分的js和css資源進行瞭一個整合,進行的操作是遷移,合並,壓縮。

建立common-resource倉庫去統一維護管理頁頭頁腳及公共資源代碼。

2.2.2.合並加載方式相同的基礎功能js並壓縮

common.js

import './common/js/AGrid';    
import './common/js/jquery.base64';    
import './common/js/lang-tips';    
import './common/js/setLocaleCookie';    
import './common/js/pepDialog';

如上面代碼,將官網中用的分散的基礎功能js合並成一個common.js,經過伏羲流水線發佈,cui套件會自動將js壓縮,這樣做的效果當然是減少官網頁面請求資源數,減小資源大小。

2.2.3.資源異步加載

觀察2.2.1中的表格可以發現,官網大部分js都是放在頭部或者是body後順序加載的,這些資源的加載時間必定是在DOMOnLoad之前

這些js都是會阻塞頁面的渲染,導致頁面首屏加載變慢,我們需要做的就是通過之前頭尾資源的整理得出哪些資源是可以在onload之後去加載的,這些我們就可以把頁面加載時不需要執行的js和css全部移到頁面渲染完成後去加載,少瞭這部分的js邏輯執行時的阻塞,頁面首屏渲染的時間也會大大降低。

通過cui套件中的feloader插件,我們可以比較便捷的控制js和css加載的時機:

feloader.onLoad(function () {    
  feloader.use([    
    '@cloud/link-to/index',    
    '@cloud/common-resource/uba',    
    '@cloud/common-resource/footer',    
    '@cloud/common-resource/header',    
    '@cloud/common-resource/common',    
    '@cloud/common-resource/prompt.css',    
    '@cloud/common-resource/footer.css',    
  ]);    
});

下圖可以明顯看到js的加載都轉移到onload之後瞭:

2.2.4 圖片壓縮

除瞭對設計給出的圖片有壓縮要求外,我們還通過對一部分不常更新的小圖標圖片進行base64編碼來減少頁面的圖片請求數量。

2.3預解析與預加載

除瞭延遲加載外,基礎模板還進行瞭諸如dns預解析,資源預加載的手段來提前解析dns和加載頁面資源。

2.3.1 DNS 預解析

當用戶訪問過官網頁面後,DNS預解析能夠使用戶在訪問雙十一活動頁之前提前進行DNS解析,從而減少雙十一活動頁面的dns解析時間,提高頁面的訪問性能,其實寫法也很簡單:

<link rel="dns-prefetch" href="//res.hc-cdn.com" rel="external nofollow" >
<link rel="dns-prefetch" href="//res-static1.huaweicloud.com" rel="external nofollow" >
<link rel="dns-prefetch" href="//res-static2.huaweicloud.com" rel="external nofollow" >
<link rel="dns-prefetch" href="//res-static3.huaweicloud.com" rel="external nofollow" >

2.3.2 preload 預加載

活動頁的部分js還使用瞭preload預加載的方式來提升頁面加載性能,preload的為什麼可以達到這種效果,我們需要看下面這段摘錄:

Preloader 簡介

HTML 解析器在創建 DOM 時如果碰上同步腳本(synchronous script),解析器會停止創建 DOM,轉而去執行腳本。所以,如果資源的獲取隻發生在解析器創建 DOM時,同步腳本的介入將使網絡處於空置狀態,尤其是對外部腳本資源來說,當然,頁面內的腳本有時也會導致延遲。
預加載器(Preloader)的出現就是為瞭優化這個過程,預加載器通過分析瀏覽器對 HTML 文檔的早期解析結果(這一階段叫做“令牌化(tokenization)”),找到可能包含資源的標簽(tag),並將這些資源的 URL 收集起來。令牌化階段的輸出將會送到真正的 HTML 解析器手中,而收集起來的資源 URLs 會和資源類型一起被送到讀取器(fetcher)手中,讀取器會根據這些資源對頁面加載速度的影響進行有次序地加載。

基於以上原理,我們對官網相對重要的js資源進行preload預加載,以使得瀏覽器可以盡快地加載頁面所需的重要資源。

<link rel="preload" href="//res.hc-cdn.com/cnpm-feloader/1.0.6/feloader.js" rel="external nofollow"  as="script"/>
<link rel="preload" href="//polyfill.alicdn.com/polyfill.min.js?features=default,es6" rel="external nofollow"  as="script"/>
<link rel="preload" href="https://res-static3.huaweicloud.com/content/dam/cloudbu-site/archive/commons/3rdlib/jquery/jquery-1.12.4.min.js" rel="external nofollow"  as="script"/>
<link rel="preload" href="//res.hc-cdn.com/cnpm-wpk-reporter/1.0.6/wpk-performance.js" rel="external nofollow"  as="script"/>

<link rel="preload" href="//res.hc-cdn.com/cpage-pep-2019nov-promotion/1.1.15/components/activity-banner/images/banner_mb.jpg" rel="external nofollow"  as="image" media="(max-width: 767px)">

優化效果

總結

前端性能優化的方法手段並不僅限於文章陳述,官網前端團隊還會在前端性能優化的道路上學習更多,探索更多,將華為雲官網頁面的加載性能做到極致!

以上就是前端從瀏覽器的渲染到性能優化的詳細內容,更多關於性能優化的資料請關註WalkonNet其它相關文章!

推薦閱讀: