這個夏季需要知道的前端效能優化小知識

語言: CN / TW / HK

我正在參加「初夏創意投稿大賽」詳情請看:初夏創意投稿大賽

文末給出了一些筆者積累的優化小知識,如果感覺文章還湊合,歡迎點贊。本文出現的任何案例均不針對個人,如有雷同純屬巧合。

寫在前面的話

這個夏天疫情還在肆虐,本文經歷了兩瓶紅牛才寫完。要知道真正寫完一篇有關效能優化的書可能需要十幾萬字,我也僅僅是將這幾年書中所讀、工作所做,以及在研發過程中的一些思考,在這個夏天寫一部分出來分享給大家。同時也在翻譯一本新書,書在國內還沒上市,期待早一天能和大家見面。

消費者或使用者不在乎你到底是不是新的技術,而是能夠帶來好的體驗或收益才最有價值。

使用者體驗資料、產品效能分析之你糊弄我我糊弄你

當前產品體驗大多都還停留感知層面,使用者體驗資料的收集更是僅僅停留在初級階段,停留在產品PPT概念層面左拼右湊出來的刀耕火種的年代的資料分析不是寥寥無幾便是蒼白無力,根本無法解釋會話跳出率為何居高不下,市場需要教育、客戶不買賬乃至客戶不懂產品邏輯的藉口一遍又一遍的自欺欺人,牛氣哄哄的前景預測與未來估值也在一個接一個的bug面前瑟瑟發抖,一句又一句口口聲聲的錢都花在了研發的刀刃上買不來投資人的信服。

  • 產品資料≠使用者體驗資料

  • 效能分析資料≠後端效能資料

通俗的說,當前真正把錢花在產品上公司或者部門屈指可數,實打實的做好產品體驗的公司更是鳳毛麟角。站在風口上飛起來的豬不會把偷偷卷市場、卷營銷的千萬花費告訴你。以十年為週期來看產品進化,精益資料分析一下網際網路產品的進化又有多少公司把錢花在了產品體驗分析上? 看一下當前研發的分配比例就能尤為說明這個問題。當前網際網路產品主要傾向於後端效能提升,而不是產品在前端的體驗,當然更不是產品的體驗上(這裡和傳統的體驗店要分開)。這裡禁不住發問:難道只有後端效能重要,前端不會瓶頸?

只有後端才會有效能瓶頸?躲在牆角飢寒交迫的前端和運維

吐槽一番之後,說到產品資源投入的話題。面向未來的業務架構、海量的分散式儲存系統,彈性伸縮的快取叢集或者分散式訊息佇列,高可用的網路秒殺系統架構,在歲月滌盪中給產品體驗提升帶來的收益有幾層樓?收益肯定是有的,不然網際網路IT民工如何成了新貴和相親排行榜的頭條。常常聽到大促、秒殺下的百億級流量架構是如何實現的,難道前端就只能是切圖仔,div的堆砌工?研發資源傾向的投入本身意味著產品結構的等級,不重視前端使用者體驗的研發結果也是能是十人成虎式的隨波逐流“業界最佳實踐”OR“業界都這麼做”

效能優化只關乎優化首屏載入?行百里半九十者大有人在

說到前端效能優化,大多想到的都是首屏載入,如果以載入時間為梯隊估計500ms以內寥寥無幾。其實優化不只在於首頁的載入,雖然這個也很重要,本文也會在後半部分做詳細的闡述,但是毋庸置疑的是前端效能優化應該體現在方方面面,這裡先舉兩個例子,然後再重點闡述一下。

例子1,長耗時任務瞭解一下?找不到也要了解一下

長耗時任務一般是超過50ms的任務。有關長耗時任務的解釋可以檢視這個網址:https://developer.mozilla.org/en-US/docs/Web/API/Long_Tasks_API

網上對於long-task的解釋遍地都是,在百度搜索long task js。

百度為您找到相關結果約20,900,000個

例子2,web-vitals瞭解一下?用不上也要偷偷卷一下

這個概念估計很多研發都瞭解過,也算難能可貴的小小的前端效能標準,這裡簡單列三個出來,分別加單的話描述是

  • LCP:主要內容出現的時間,越短越好

  • FID:輸入延遲的時間,低於100ms越好

  • CLS:頁面變化的積累量,低於0.1越好

有關這些詳細內容可以在https://www.npmjs.com/package/web-vitals上進行檢視,稍微懂點前端的同學都可以引用將資料進行上報整理成頁面的資料出來。如下:

npm install web-vitals
import {getLCP, getFID, getCLS} from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getLCP(console.log);

通常將得到的結果,加上常見的performance Api就能比較淺顯的瞭解前端大概系統的效能。本來想把掘金的performance.getEntries()的結果拷貝出來,結果掘金出現了最大字數的限制,只好貼圖了。

如果將web-vitals和performance進行結合,基本就能看出一個網站的效能資料,其中performance中內容簡介如下:

performance timing api 主要包含三個部分navigation timing:頁面載入過程的效能資料resource timing:指令碼樣式等資源載入的效能資料user timing:記錄不同程式碼片段的執行時間提供的apiwindow.performance.getEntriesByType("resource") 獲取資源相關的效能資訊

然並卵這遠遠不是開始,真正的效能優化在於建立資料基線,選定關鍵指標、制定效能預算、以及設定目標值。

紙上談兵沒有資料的都是耍流氓,升職加薪的都是寫PPT的?

管理學大師彼得·德魯克:If you can't measure it, you can't improve it.“如果你不能夠衡量,那你就不能有效的增長。”

如果僅僅只是本地看看web-vitals和performance,產品經理笑話只會ctrl+c和ctrl+v的前端程式設計師還真的一點也不冤枉。建立資料基線進行分析總結從而各個擊破才能王者無敵。

uploadDataToServer(performance Data)
.then(analyze)

以long-tasks為例子,至少需要long-task的時序資料、頁面分佈

任何一處程式碼的優化都是有益的,研發也不能看到一行優化一行程式碼,尋找最有益的切入點就顯得尤為重要,首先研發要知道使用者在一定時間段內的長耗時任務的分佈情況。

圖1 長耗時任務時序分佈,據此可以得到可以得到優化的一點點基線資料而不是簡簡單單的PPT。

圖2長耗時任務在不同頁面的時序分佈,可以針對頁面進行優化排等級,如/statics頁面出現的長耗時任務較多,可能需要優化的優先順序就可能會靠前

圖3 長耗時任務在不同頁面的時序分佈,可以針對頁面進行篩選來安排優化任務。

當然以上的圖也有可能是多種多樣,比如長耗時頁面排行或者TOPN長耗時頁面來有針對性的進行優化。

圖4 長耗時任務頁面分佈排行

圖5 TopN 長耗時頁面,如果看了TopN還不知道效能優化從哪裡入手,也就只有孫悟空才能拯救你的系統,讓他72變吧。

以上僅僅是兩個簡單的例子來說明前端效能優化的一些可以進行的點,業界更多是對首屏載入耗時的統計,資料可能如下

圖6 載入耗時統計

前端優化能做哪些?

根據上圖前端整體的優化一般可以劃分為建立連線、請求響應兩個階段,從建立連線階段前端多是對dns-prefetch or Cdn的就近獲取;在請求響應階段,以http協議頭為基準對檔案進行按需載入、預載入為代表或service woker式或server push式的策略。

以chrome為例,在建立連線階段客戶端最多與主機建立6個tcp連線,通過劃分子域方式,將多個資源分佈在不同子域上,減少請求佇列的等待時間可算得上一種優化的方式,然而劃分子域並不是一勞永逸的方式,畢竟更多子域意味著更多的DNS查詢時間。所以有了提前建立dns的偽命題出現(為什麼是偽命題?),如下所示

<link rel="dns-prefetch" href="protocol://cdndomain/">

技術非靜止,效能優化高度依賴技術加持和流程消減。反覆建立連線耗時對於毫秒必爭的前端來說達到了錙銖必較的程度,一切以headers為至上的資源載入,有了keep-alive能夠使得通訊仍然保持一定時間,減少在一個單獨連線結束後再次進行tcp連線,但keep-alive以及增加link節點距離真正的資源(css/js/image/fonts)節省之間還不是天差地別,gzip曾一度照亮整個前端世界,keep-alive和dns-prefetch節省的幾ms在gzip對整個網路以百分比減小的激流下殺出了幾十ms乃至幾百ms,再後來http2更是彷彿一道光帶給前端同學希望,頭部壓縮、cookies服用、多路複用乃至server push在一定程度緩解了建立連線和請求響應的阻塞狀態。

#gzip的相關介紹
https://developer.mozilla.org/en-US/docs/Glossary/GZip_compression

#http2的相關介紹
https://developer.mozilla.org/en-US/docs/Glossary/HTTP_2

然而遺憾的說從整體來說,前端仍然沒有突破以網路和資源為限定的效能瓶頸。資源載入整階段仍然停留在500ms以上。

資源載入完了就可以高枕無憂啦?

1.頁面渲染了多少節點,dom節點有多重要?

稍微寫過幾行div的同學都知道頁面是基於DOM樹的構建和CSS樹的結合構成的Render樹。這裡引用一本種有關效能優化用一個簡單但非完全準確的公式來表現這個過程的複雜度

M:代表Dom節點的數量
N:代表Css節點的數量
Z:遍歷迴圈的次數

Z=M*N

隨附一段計算頁面節點的方法如下,可以用這段程式碼來檢查頁面中的node節點的數量。

function countNodes(node) {
    let count = 1;
    //  判斷是否存在子節點
    if(node.hasChildNodes()) {
        //  獲取子節點
        var cnodes = node.childNodes;
        //  對子節點進行遞迴統計
        for(var i=0,len=cnodes.length; i<len; i++) {
            count  += countNodes(cnodes.item(i))
        }
    }
    return count;
}
//  統計body的節點數量
countNodes(document.body)

我們隨機選擇了70家網站來統計首屏dom節點數量,包含頭條、掘金、藍湖、嗶哩嗶哩、天貓、火山引擎、京東商城等,詳細公司網站清單如下

網易雲音樂、好看影片、鳳凰資訊、思否、天貓首頁、拼多多、知乎首頁、懂車帝
西瓜影片、華為商城、優酷網、瓜子二手車、抖音網頁版、驢媽媽、騰訊新聞、貝殼網
頭條首頁、噹噹網、唯品會、企查查、豆瓣音樂、掘金、京東商城首頁、新華網、馬蜂窩
愛企查、攜程、東方網、百度小說、中國日報、鬥魚TV、網易公開課、美團、人民網、前程無憂
藍湖、csdn、中國體彩、火山引擎、qq音樂、縱橫中文網、光明網、聚划算、雪球首頁
hao123、2345導航、鏈家二手房、北航、飛豬、蘇寧易購、驢媽媽旅遊、58同城、58二手車
中金線上、天眼查、我愛我家、小米商城、愛奇藝、拉勾招聘、同花順財經、芒果tv、新浪體育
騰訊體育、搜狐、轉轉、嗶哩嗶哩、途牛、安居客、boss直聘、房天下、新浪新聞、東方財富、
易車、騰訊影片、汽車之家

在3000個節點以下的公司有:

3000-6000個節點以內的公司資料如下

dom節點數量超過6000以上的公司如下,其中汽車之家dom節點最多,有21010個dom節點。

注:
1.dom節點的排名並不代表公司網站效能,僅僅是為估算大概頁面節點數量來做一個評估
2.僅考慮首頁也就是一級頁面。(未做下滑載入更多或者分頁的有點吃虧)
3.首頁dom節點可不是整個螢幕的節點數量,也包含使用者不可見的dom節點

以上70家公司節點在一千和兩萬之間,平均數為4661,中位數為3881。我們姑且以3800個Dom節點,2000個css節點來計算頁面渲染時需要的遍歷次數:

 z=3800*2000=720萬

我們為什麼要統計dom節點和css節點數量呢?因為目前dom節點數量與頁面渲染息息相關。

dom節點在渲染樹中至少存在一個對應的幀節點,這個幀節點有寬高、內外邊距和座標。一旦渲染樹構建完成,瀏覽器便會開始繪製頁面。

每當頁面出現:dom刪除或增加、元素位置或尺寸的屬性、元素內容乃至瀏覽器視窗變化,都會出現重排,大多數瀏覽器都通過佇列和批量執行優化重排。

以汽車之家2萬的dom節點為例見下圖,如果切換tab效果如下所見(也許本來就是空白,誰讓你節點最多)

以上切換均在網路狀況良好下進行,如果在惠新西街南口望京地鐵換乘期間(以前上下班在此處換成時基本無網或者網路條件超級差),開啟汽車之家的網站,使用者體驗得有多差。所以為了效能,減少dom節點或者對dom的操作,有的網站甚至有了使用偽元素、批量操作dom(fragment 或者其他)、虛擬dom等的優化方法。

雖然Dom是個獨立語言,也就是HTML標籤但實際中基本上來源於動態建立,可能來源於php或者後端語言,也可能來自於ajax從後端獲取在前端進行拼接。

比如php

   <?php foreach ($row as $num => $info) { ?>        <div>            <div data-label="id">                <?= $num+1 ?>            </div>            <div data-label="username">                <?= $info["username"]  ?>            </div>            <div data-label="password">                <?= $info["password"]   ?>            </div>            <div data-label="phone">                <?= $info["phone"]   ?>            </div>        </div>    <?php } ?>

比如vue

<template>  <div class="demo">    <div v-for="(item,index) in list" :key="index">      <img :src="item.src" alt="">    </div>    </div></template>

比如react

  render() {    const getItem = this.props.breadcrumb.map((item, index) => {      if (item.href) {        return (          <Breadcrumb.Item key={index}>            <Link to={item.href}>{item.name}</Link>          </Breadcrumb.Item>        );      } else {        return <Breadcrumb.Item key={index}>{item.name}</Breadcrumb.Item>;      }    });    return (      <div className="breadcrumb-container">        <Breadcrumb>{getItem}</Breadcrumb>      </div>    );  }

但無論是通過什麼語言進行建立,瀏覽器都需要對dom進行解析和渲染。

2.頁面載入的效能資料有哪些

我們上面收集的dom數量就跟瀏覽器渲染的時間段息息相關,但在dom出現之前能收集到的技術性能指標資料還有很多。

網路層的:頁面域名解析時間、tcp時間、ttfb時間、download時間
瀏覽器渲染:頁面開始的時間、domready的時間、Pageload 時間
頁面載入詳細資料:css、js、image、fonts等消耗的時間

根據以上我們基本能總結出來一些效能優化的方式和手段,我結合雅虎14條軍規做了以下總結:

  1. 使用分子域名載入資源
  2. 使用較近的CDN或dns預解析
  3. 使用高效能傳輸方式或方法,http2,quic,gzip...
  4. 減少http請求的數量,合併公共資源、使用雪碧圖、合併程式碼塊、按需載入資源
  5. 減少傳輸總量或加快傳輸速度

  6. 優化圖片的載入展示策略,根據網路狀況載入圖片、圖片格式優化、圖片展示位置優化

  7. 減少cookie體積

  8. 使用更有效的快取策略,keep-alive,expiration,max-age...

  9. 使用良好的頁面佈局

  10. 10合理安排路由策略
  11. 減少反覆操作dom

  12. 減少重繪和重排

  13. 非同步載入資源
  14. 公用css類
  15. 使用GPU渲染初始動畫和圖層的合成
  16. 高效的js程式碼
  17. 使用防抖和節流對UI進行優化
  18. 使用web worker載入資源
  19. 減少301 302
  20. 試試快取資料的方法localStorage/sessionStorage/indexedDB
  21. 無阻塞載入js,減少併發下載或請求
  22. 減少外掛中的多語言版本內容
  23. 減少佈局上的顛簸,減少對臨近元素的影響
  24. 減少同時的動畫
  25. 制定弱網精簡策略
  26. 針對裝置制定精簡策略
  27. 減少頁面圖層
  28. js、css命名儘量簡短
  29. 減少js全域性查詢
  30. 減少迴圈和迴圈巢狀以減少js執行時間
  31. 減少事件繫結
  32. 元件提取、樣式提取、函式提取
  33. 按照頁面變更頻率安排資源
  34. 減少iframe
  35. 注意頁面大小,特別是canvas的大小和佔用記憶體

寫在最後的話

本文沒有章法,想到哪裡寫到哪裡,本來還打算寫幾個標題:

效能度量會改變被觀測者的效能?
呼之欲出的web 3.0能拯救500ms的限制?
資訊架構什麼時候能應用到前端領域?

但是掘金的寫作體驗真的是太差了,而且在寫作過程中bug頻出。不過目前掘金確是難得的幾種傳播方式之一。

點名婚貝網站.朋友發來婚禮邀請,開啟之後是這個樣子

點名掘金頁面.滾屏出現了什麼鬼

點名汽車之家 這裡空白是什麼產品體驗?