使用 Lighthouse 分析前端性能

blank

使用 Lighthouse 分析前端性能

lighthouse 是 Google Chrome 推出的一款開源自動化工具,它可以搜集多個現代網頁性能指標,分析 Web 應用的性能並生成報告,為開發人員進行性能優化的提供了參考方向。

使用入門

在 Chrome DevTools 中使用

比較推薦的方法。 我們可以直接打開調試面板中的 「Lighthouse」面板,然後點擊生成報告:

blank

使用 Chrome 外掛程式

我們可以在應用商城下載並將外掛程式添加到瀏覽器,點擊外掛程式面板開始分析,使用方法和上面類似。

在 Node 命令行工具中使用

在調試面板和外掛程式中只能進行一些基本的配置,如果我們想要更加靈活地配置分析的內容,我們可以在 Node 命令行工具中使用:

  • 安裝
npm install -g lighthouse
# or use yarn:# yarn global add lighthouse
  • 分析:以 www.baidu.com 為例,使用以下命令可以生成一份中文報告:
lighthouse https://www.baidu.com/ --locale=zh-CN --preset=desktop --disable-network-throttling=true --disable-storage-reset=true

blank

  • 登錄驗證:有的時候,我們需要拿到許可權才能訪問頁面,這個時候,直接執行以上的命令會報錯或者分析到的不是目標頁。 這裡,需要我們手動登錄或者藉助 Puppeteer 模擬登錄。 參考文件,我們可以這樣處理:
  • 執行 chrome-debug 啟動 Chrome 實體(全域安裝了 lighthouse 之後)
  • 訪問我們的目標網址並完成登錄
  • 在新的終端,運行上面的命令 lighthouse [https://example.com](https://www.baidu.com/) --preset=desktop --port=chrome.port
  • 更多的設定見 cli-options

使用 Node module

我們還可以在自己的本地專案中,將 Lighthouse 作為一個 module 使用:

  • 安裝
yarn add --dev lighthouse
  • 自訂啟動
constfs=require("fs");constlighthouse=require("lighthouse");constchromeLauncher=require("chrome-launcher");constlaunchChromeAndRunLighthouse=async(url,opts,config=null)=>{constoptions={logLevel:"info",output:"html",onlyCategories:["performance"],...opts,};// 打开 chrome debug
constchrome=awaitchromeLauncher.launch({chromeFlags:opts.chromeFlags});// 开始分析
construnnerResult=awaitlighthouse(url,{...options,port:chrome.port},config);awaitchrome.kill();// 生成报告
constreportHtml=runnerResult.report;fs.writeFileSync("lhreport.html",reportHtml);};// 启动
construn=async()=>{constopts={chromeFlags:["--show-paint-rects"],preset:"desktop",};awaitlaunchChromeAndRunLighthouse("https://example.com",opts);};run();

性能指標

用戶關心什麼

性能評估的最終目的是提升使用者的使用體驗。 因此,性能指標的制定也要從使用者的角度出發。 通常來說,從網站載入到可以正常使用,我們需要關注以下幾個關鍵節點的使用者感知體驗:

blank

  • 根據位址是否能正確訪問到網頁? 伺服器請求是否正常回應?
  • 頁面是否渲染了使用者可用的、關心的內容?
  • 頁面是否可以正常回應使用者的交互操作?
  • 使用者的交互回應是否流暢自然?

從這些角度出發,我們在 這裡 看下 Lighthouse 提供的幾個性能評估指標:

blank

Lighthouse 最新版的提供了 6 個性能指標:FCPSILCPTTITBTCLS;權重分別是 15%,15%,25%,15%,25% 和 5%。 Lighthouse 會根據權重計算得到一個分數值。

內容呈現相關

FCP

FCP(First Contentful Paint)即首次內容繪製。 它統計的是從進入頁面到首次有 DOM 內容繪製所用的時間。 這裡的 DOM 內容指的是文本、圖片、非空的 canvas 或者 SVG。 我們也可以在 Performance 面板看到這個指標:

blank

FCP 和我們常說的白屏問題相關,它記錄了頁面首次繪製內容的時間。 一個常見的影響這個指標的問題是:FOIT(flash of invisible text,不可見文本閃爍問題),即網頁使用了體積較大的外部字體庫,導致在加載字體資源完成之前字體都不可見。 可以通過 font-display API 來控制字體的展示來解決。

但值得注意的是,頁面首次繪製的內容可能不是有意義的。 比如頁面繪製了一個佔位的loading圖片,這通常不是使用者所關心的內容。

LCP

前面我們已經提到了,由於FCP指標統計的內容可能不是使用者主要關心的,那麼我們需要另一個指標來評估。

LCP(Largest Contentful Paint)即最大內容繪製。 它統計的是從頁面開始載入到視窗內最大內容繪製的所需時間,這裡的內容指文本、圖片、視頻、非空的 canvas 或者 SVG 等。

LCP 之前,lighthouse 還使用過 FMPFirst Meaningful Paint,首次有意義內容繪製)指標。 FMP 是根據布局物件(layout objects)變化最大的時刻來決定的。 但是這個指標計算比較複雜,通常和具體的頁面以及瀏覽器的實現相關,這也會導致計算不夠準確。 比如,用戶在某個時刻繪製了大量的小圖示。

Simpler is better! 使用者感知網頁的載入速度以及當前的可用性,可以簡單地用最大繪製的元素來測量。 LCP 指向的最大元素通常會隨著頁面的載入而變化(只在使用者交互操作之前),以下是一個網站的範例:

blank

SI

SI(Speed Index)即速度指數。 Lighthouse 會在頁面載入過程中捕獲視頻,並通過 speedline 計算視頻中幀與幀之間視覺變化的進度,這個指標反映了網頁內容填充的速度。 頁面解析渲染過程中,資源的載入和主線程執行的任務會影響到速度指數的結果。

CLS

前面幾個指標關注的都是頁面呈現的快慢,但是很多時候我們希望頁面的視覺呈現保持相對穩定,比如,不會突然插入一張圖片或者元素突然發生位移。

這個時候,Lighthouse 使用 CLS(Cumulative Layout Shift)即累計佈局位移進行評估。 這個指標是通過比較單個元素在幀與幀之間的位置偏移來計算,計算公式是 cls = impact fraction * distance fraction 。 在以下例子中,文本塊在兩幀之間的 impact fraction 是紅色框部分,佔視窗 75%; distance fraction 是藍色箭頭的距離,佔視窗 25%;那麼最終的分數是 0.75 * 0.25 = 0.1875

blank

使用者交互相關

TTI

TTI(Time To Interactive)即頁面可交互的時間。 這個時間的確定需要同時滿足以下幾個條件:

  • 頁面開始繪製內容,即 FCP 指標開始之後
  • 使用者的交互可以及時回應:
  • 頁面中大部分可見的元素已經註冊了對應的監聽事件(通常在 DOMContentLoaded 事件之後)
  • TTI 之後持續 5 秒的時間內無長任務執行(沒有超過 50 ms 的執行任務 & 沒有超過 2 個 GET 請求)

blank

TBT

TBT(Total Blocking Time)即阻塞總時間,測量的是 FCPTTI 之間的時間間隔。 這個指標反映了使用者的交互是否能及時回應。 如果主線程執行了長任務會導致使用者的輸入無法及時回應。 當主線執行的任務所需的時長超過 50ms,我們就認為這是一個長任務(long task)。 假設在主線程上執行了一系列的任務,每個長任務的阻塞時間等於執行時間減去 50 ms,最後可以統計得到一個總的阻塞時間。

blank
blank

性能優化

我們可以在報告的 Opportunities 一節看到影響評估的關鍵原因,並進行一些優化:

blank

優化 JavaScript 資源載入

blank

在 lighthouse 的優化建議中,我們可以看到和 JavaScript 資源載入相關的建議有:

  • Reduce unused JavaScript
  • Minify JavaScript
  • Remove duplicate modules in JavaScript bundles

資源載入的優化通常有幾個思路:

  • 合理的載入順序/策略(延遲載入/預先載入)
  • 壓縮優化資源的體積
  • 代碼分割 & 公共提取 & 按需載入

分析

如何確定頁面中載入了哪些不必要的資源呢? 我們可以打開 Chrome Devtool Coverage 面板,查看當前使用資源的代碼覆蓋率,紅色表示未使用到的代碼。

blank

在 webpack 專案里,可使用 webpack 的外掛程式 webpack-bundle-analyzer 來分析,如下我們啟動一個本地埠來查看 webpack 的打包情況。

newBundleAnalyzerPlugin({analyzerMode:"server",analyzerHost:"127.0.0.1",analyzerPort:8888,reportFilename:"report.html",defaultSizes:"parsed",openAnalyzer:true,statsFilename:"stats.json",statsOptions:{exclude:["vendor","webpack","hot"],},excludeAssets:["webpack","hot"],logLevel:"info",});

打包後會生成如下的報告,我們可以查看模組打包的情況,還可以切換 Stat / Parsed / Gizzped 來查看開啟壓縮後代碼體積的變化:

blank

Code Splitting

減少無用的代碼,首先我們需要更加精細合理地切分代碼。 在 webpack 專案中,通常有三種代碼分割的技巧:

  • 多入口:基於 entry 配置,每個入口打包成單獨的 bundle
constpath=require("path");module.exports={entry:"./src/index.js",mode:"development",entry:{index:"./src/index.js",another:"./src/another-module.js",},output:{filename:"main.js",filename:"[name].bundle.js",path:path.resolve(__dirname,"dist"),},};
  • 抽離公共代碼:如果多個頁面使用了公共的模組,可以通過 SpitChunksPlugin 將代碼分割成多個chunks:
module.exports={//...
optimization:{splitChunks:{chunks:"async",// 代码分割时只对异步代码生效;all 全部生效;initial 同步代码生效
minSize:20000,// 代码分割的最小体积,
minChunks:1,// 做代码分割的最少引用次数
maxAsyncRequests:30,// 同时加载模块的最大数量
cacheGroups:{venders:{chunks:"all",test:/[/]node_modules[/]/,// 将 node_modules 中的代码进行分割
priority:-10,// 代码分割的优先级
reuseExistingChunk:true,// 已经打包过的代码直接复用
},default:{minChunks:2,priority:-20,reuseExistingChunk:true,},},},},};W;
  • 動態載入:動態載入通常是和上面兩種方法結合使用。 在打包階段,通過動態 import 引入的模組會被 webpack 單獨拆分成一個 chunk ;在運行的階段,webpack 會通過 chunkId 判斷腳本是否已載入並緩存過,沒有的話再通過 script 標籤動態插入。 在 React 專案中,可以結合 React.lazySuspense 進行路由的切分。
import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </Router>
);

DCE

搖樹優化(Tree-Shaking)是用於消除死代碼(Dead-Code Elimination)的一項技術。 這主要是依賴於 ES6 模組的特性實現的。 ES6 模組的依賴關係是確定的,和運行時狀態無關,因此可以對代碼進行可靠的靜態分析,找到不被執行不被使用的代碼然後消除。

對於無用的導入和變數,我們很容易通過IDE提示發現。 而webpack會在 production 模式下自動進行一些優化,比如移除無用的導出模組:

mode:'development',optimization:{usedExports:true,},

代碼壓縮

terser-webpack-plugin(webpack5 自帶)可以用來壓縮代碼和清理無意義的代碼:

constTerserPlugin=require("terser-webpack-plugin");module.exports={optimization:{minimize:true,minimizer:[newTerserPlugin()],},};

之前提到的都是在打包階段進行優化。 除此之外,壓縮資源體積更加有效的方法是開啟 gzip ,在 nginx 上我們可以很簡單地進行配置:

gzipon;gzip_min_length1k;gzip_buffers416k;gzip_http_version1.1;gzip_comp_level9;gzip_typestext/plainapplication/x-javascripttext/cssapplication/xmltext/javascriptapplication/x-httpd-phpapplication/javascriptapplication/json;gzip_disable"MSIE[1-6].";gzip_varyon;

考慮腳本載入的順序

當 HTML 解析時遇到 script 文本會暫停 DOM 樹的構建,非異步的腳本需要等待瀏覽器下載、解析、編譯和執行,這會導致渲染的延遲。 因此,對於頁面中腳本的引用我們需要謹慎地權衡:

  • 關鍵的腳本內聯使用,減少不必要的網路等待時間
  • 其他的腳本在文檔底部引入,或者通過 async/defer 異步引用

優化 CSS 資源載入

blank

同樣地,lighthouse 給出了一些 CSS 資源優化的建議:

  • Reduce unused CSS
  • Minify CSS

按需載入

在 webpack 中我們可以使用 mini-css-extract-plugin,將 CSS 檔從 JS 檔中單獨抽取出來,支援按需載入:

newMiniCssExtractPlugin({filename:"[name].[hash:8].css",chunkFilename:"[name].[contenthash:8].css",}),

資源壓縮

對於 CSS 的壓縮,我們可以使用 optimize-css-assets-webpack-plugin 移除無用的代碼:

newOptimizeCssAssetsPlugin({assetNameRegExp:/.optimize.css$/g,cssProcessor:require("cssnano"),cssProcessorPluginOptions:{preset:["default",{discardComments:{removeAll:true}}],},canPrint:true,});

代碼優化 & 模組化

除了藉助工具來優化 CSS 資源,在開發中我們也要注意一些代碼的組織和複用,例如下面這個例子:

h1{background-color:#000000;}h2{background-color:#000000;}//减少文件代码h1,h2{background-color:#000000;}

隨著反覆運算的開發,專案中很可能會冗餘了大量不再需要的 CSS 代碼。 在前端元件化的時代,將樣式和元件綁定,使用更加模組化的方案或許是更加有效的策略。 我們可以使用一些 CSS-in-JS 的方案,比如 styled-components,也可以瞭解下 Atomic CSS(原子 CSS)

優化圖片的載入

blank

關於圖片的載入,lighthouse 給出了很多方面的建議:

  • Defer offscreen images
  • Serve images in next-gen formats
  • Efficiently encode images
  • Properly size images

圖片懶載入

圖片的請求和載入通常需要佔用較大的資源,特別是在一些以圖片展示為主的電商網站上。 對於當前頁面不可見的圖片,我們可以採用懶載入的方式進行處理。

// html
<imgdata-src="http://example.com"/>;// js
constlazyLoadIntersection=newIntersectionObserver((entries)=>{entries.forEach((item,index)=>{if(item.isIntersecting){item.target.src=item.target.dataset.src;lazyLoadIntersection.unobserve(item.target);}});});constimgList=document.querySelectorAll(".img-list img");Array.from(imgList).forEach((item)=>{lazyLoadIntersection.observe(item);});
  • 我們也可以通過原生 JS 實現:
constisInView=(el)=>{const{top,left,height,width}=el.getBoundingClientRect();constclientHeight=window.innerHeight||document.documentElement.clientHeight;constclientWidth=window.innerWidth;document.documentElement.clientWidth;if(top<clientHeight&&left<clientWidth){returntrue;}returnfalse;};constlazyload=()=>{constimgLsit=document.querySelectorAll(".img-list img");if(!imgLsit.length){document.removeEventListener("scroll",throttleScroll);return;}imgLsit.forEach((img)=>{if(!img.src&&isInView(img)){img.src=img.dataset.src;}});};lazyload();constthrottleScroll=throttle(lazyload,300);document.addEventListener("scroll",throttleScroll,{passive:true});

回應式圖片

除了懶載入,在一些回應式的網站,我們往往希望圖片可以根據設備型號的不同,載入不同解析度和大小的圖片,這可以使用 Responsive images 方案來解決。 主要是基於 srcset 屬性:

<imgsrcset="elva-fairy-480w.jpg 480w,
             elva-fairy-800w.jpg 800w"sizes="(max-width: 600px) 480px,
            800px"src="elva-fairy-800w.jpg"alt="Elva dressed as a fairy">

WebP 圖片格式優化

WebP 是由谷歌(Google)推出的一種旨在加快圖片載入速度的圖片格式,在相同質量的情況下,WebP 的體積要比 JPEG 格式小 25% ~ 34%。 目前,一些圖片依託網站也提供了webp的支援,我們在使用之前需要檢測當前瀏覽器的相容情況。

<picture><sourcetype="image/svg+xml"srcset="pyramid.svg"><sourcetype="image/webp"srcset="pyramid.webp"><imgsrc="pyramid.png"alt="regular pyramid built from four equilateral triangles"></picture>

優化網路請求

blank

針對網路連接的建立和請求的緩存,lighthouse 也給出了一些參考的建議:

  • Use HTTP/2
  • Serve static assets with an efficient cache policy
  • Preload key requests
  • Preconnect to required origins
  • Avoid enormous network payloads
  • Fonts with font-display: optional are preloaded
  • Preload Largest Contentful Paint image

CDN 加速

我們可以將一些資源,比如圖片、公共類庫放到 CDN 上,基於內容分發和緩存提高回應速度,阿裡雲的 OSS 還提供了圖片的多格式轉換。

內容分發網路(Content Delivery Network,簡稱 CDN)是由分佈在不同區域的邊緣節點伺服器群組成的。 CDN 可以將源站內容分發到距離使用者最近的節點,從而提高回應速度和成功率。 其底層是依賴 DNS(功能變數名稱解析服務)傳回給使用者最近的IP位址實現的。

合理的 HTTP 快取策略

對於放置在 CDN 的資源,我們需要設置合理的 HTTP 快取策略來資源的命中和載入。 HTTP 有兩種緩存機制,強緩存(基於 Expires 和 Cache-Control)以及協商緩存(基於 Last-Modified/if-Modified-Since 或 Etag / If-None-Match)。

  • 對於一些不需要經常變化的資源,或者 webpack 中每次打包會根據內容生成檔路徑( contenthash )的資源,我們可以設置一個比較大的緩存時長或者過期時間
  • 而對於首頁 index.html 等頁面我們應該禁止緩存,或者設置一個比較短的緩存時間並檢查資源的新鮮度

使用 HTTP/2

我們知道在 HTTP/1.1 中,瀏覽器對於同一個功能變數名稱的 tcp 連接數有限制,通常只能同時發起 6 ~ 8 個連接。 同時發起多個請求,在 Network 面板我們可以看到明顯的階梯式變化以及很多的 Queueing 排隊等待的時間:

blank

HTTP2 增加了多路複用、流優先順序、頭部壓縮等功能,可以很好地解決 tcp 連接限制和隊頭阻塞問題。 我們可以在 nginx 在配置,需要結合 https 使用:

listen443sslhttp2;

使用 pre-* 預處理

  • preload:通過 rel="preload 對優先順序較高的資源進行內容預載入,比如一些重要的樣式、字體檔和圖片,我們不希望頁面在顯示的時候出現閃動,可以預載入;也可以使用 preload-webpack-plugin;
<linkrel="preload"href="main.js"as="script"/><linkrel="preload"href="fonts/cicle_fina-webfont.ttf"as="font"type="font/ttf"crossorigin="anonymous"/>
  • dns-prefetch:當頁面通過網址請求伺服器的時候,需要先通過 DNS 解析拿到 IP 位址才能發起請求。 如果網站存在大量跨域的資源,DNS 的解析過程很可能會降低頁面的性能。 對於關鍵的跨域資源,我們最好進行 dns 預獲取,還可以結合 preconnect 進行預連接:
<linkrel="preconnect"href="https://fonts.gstatic.com/"crossorigin/><linkrel="dns-prefetch"href="https://fonts.gstatic.com/"/>
  • prefetchpreload 通常用於預載入當前頁面的一些關鍵資源,如果我們想預獲取將要用到的資源或將要導航到的頁面,可以使用 prefetch ;

優化頁面渲染性能

blank

我們知道,瀏覽器的頁面渲染通常需要經過構建 DOM 樹,構建 CSSOM 樹,構建渲染樹、樣式計算和佈局繪製合成幀並繪製到螢幕幾個過程。 在這個過程中,主線程複雜的 JS 任務會阻塞渲染。

針對解析和渲染的過程,lighthouse 也提出了一些優化建議:

  • Avoid an excessive DOM size
  • Avoid large layout shifts
  • Avoid non-composited animations
  • Image elements do not have explicit width and height

高性能的動畫

blank

當我們進行一些樣式變換,比如改變元素的寬高時,瀏覽器需要重新計算元素的幾何元素和樣式,這個過程可能需要耗費一些時間。 合成線程可以開啟硬體加速且無需等待主線程計算,因此,動畫應該盡可能放到合成線程處理。 通常有幾種方法:

  • 使用 transform: scale() 代替 widthheight
  • 使用 transform: translate() 代替 top 、 、 、 right bottom left 屬性

同時,要保證這些元素存在合成圖層中,通常可以通過提升元素(在父元素中聲明)來解決:

  • will-change: transform
  • transform: translate3d(0, 0, 0)

避免執行長任務

  • 通過時間分片(Time Slicing),將複雜的任務拆分成多個異步執行的任務
  • 將複雜的任務放到 worker 線程中,計算有了結果再通知主線程

參考資料

lighthouse

Lighthouse performance scoring

webpack

HTTP/2: the difference between HTTP/1.1, benefits and how to use it

How Browsers Work: Behind the scenes of modern web browsers

想了解更多關於 Facebook 與 Google 廣告投放?

What do you think?

Written by marketer

Chrome團隊:如何曲線拯救KPI

blank

【性能優化】性能測量工具-LightHouse