如何從 0 到 1 搭建性能檢測系統

這是第 74 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: 如何從 0 到 1 搭建性能檢測系統

前言

前端頁面性能對用戶留存、用戶直觀體驗有着重要影響,當頁面加載時間超過 2 秒後,加載時間每增長一秒,就會有大量的用戶流失,因此作好頁面性能優化,無疑對網站來講是一個很是重要的步驟。前端

那如何才能知道一個頁面的性能狀況呢?知道了頁面性能狀況後又如何進行優化呢?一個頁面的性能指標很是多,面對一大堆性能指標,可能一個老手也一時間不知道從何開始分析。並且不一樣團隊,負責的業務不一樣,性能分析的指標也不可以一律而論。打個比方說,對於通常的電商網站,必定會有不少圖片,那圖片加載的性能提高對網站的性能提高做用就比較大。而對於一些由表單組成的中臺頁面,提高圖片加載速度的收益遠小於電商網站。vue

總結來講,不一樣的團隊有着各自不一樣的業務,業務之間千差萬別,性能指標也不能一律而論,因此用一套統一的檢測模型覆蓋全部場景是不現實的。本文將介紹如何定製一個屬於本身團隊的性能檢測平臺。node

先看下政採雲的性能檢測平臺——百策react

圖片

在聊性能指標以前,先講一下 Lighthouse。git

Lighthouse

Lighthouse 是一個開源的自動化工具,用於分析和改善 Web 應用的質量。運行 Lighthouse 共有 4 種方式,分別在 Chrome 開發者工具,Chrome 擴展程序,Node CLI 和 Node module。百策主要基於 Node module 方式,在其基礎上進行擴展開發,Lighthouse 詳細使用參見 Git:https://github.com/GoogleChrome/lighthousegithub

下圖爲 Lighthouse 檢測頁面性能的一個最終結果,能夠看到其實指標已經比較完善了。vue-router

可能有人會問,爲何不直接使用 Lighthouse。首先,因爲不可描述的緣由,國內直接使用 Chrome 開發者工具中的 Lighthouse 時,會一直處於 Lighthouse is warming up 狀態。其次,Chrome 擴展程序對於須要登陸的頁面也不支持。最後,對於前言中,某一些定製需求 Lighthouse 也不能全然知足,因此要基於 Lighthouse 進行定製,作一個知足業務要求的性能檢測平臺。chrome

總體設計架構

下圖是百策系統的一個總體架構typescript

  • 前端主要使用的是 Antd 和 Antd Charts,包含常規頁面的展現和部分性能走勢圖表的展現。
  • 服務端基於 nestjs 開發,接入 Sentry 作報警監控。helmet 用於保護系統免受一些衆所周知的 Web 漏洞影響。
  • node-schedule 用於每週定時計算已統計入系統的頁面性能,並經過 nodemailer 發送郵件。
  • Compression 主要用於啓用 gzip。
  • 最主要的檢測服務基於 Puppeteer 和 Lighthouse 開發。

百策採集頁面性能數據的流程

百策系統監控頁面的方式主要採用的方式是合成監控,對於什麼是合成監控,能夠參考此文章:螞蟻金服如何把前端性能監控作到極致。總結來講,合成監控的優點就是:可以採集的數據更豐富,而且能夠根據不一樣的場景定製不一樣的運行環境等。首先百策要根據不一樣的場景,好比政採雲前臺頁面、政採雲中臺頁面制定不一樣的檢測模型。其次百策的主要目標是提高頁面性能,而且須要保證環境和硬件條件一致的狀況下對頁面作性能比對,因此選擇採用合成監控更加適合。數據庫

先看下 Chrome Lighthouse 的架構圖(圖來源於 Lighthouse Git),主要基於 4 個主要步驟實現,分別是交互驅動,收集,審計以及記錄組成,參考了 Chrome Lighthouse,百策的檢測模型邏輯也主要由這 4 步組成:

一、頁面交互後,發起請求調用服務。

二、遍歷當前頁面所須要的收集器,合併爲一個總的收集器,並採集數據。

三、將第二步採集到的數據作性能計算和評分。

四、將性能檢測結果存入數據庫。

百策採集頁面性能數據的實現方案

百策實現頁面性能數據採集的方案主要依靠無頭瀏覽器 Puppeteer 結合 Lighthouse,Puppeteer 是 Chrome 團隊提供的一個無界面 Chrome 工具,人稱無頭瀏覽器,經過 API 來控制 Node 端的 Chrome。百策的主要邏輯是在服務端起一個無需顯示的 Chrome,經過 Lighthouse 的 API 新建一個標籤頁並打開,Lighthouse 會計算具體的性能指標,具體的檢測邏輯能夠參考下圖。接下來我會用關鍵代碼說明如何實現其中的關鍵步驟。

○ 開始入口

如下是百策價值 1 個億的代碼,主要流程以下,鉤子函數是用於在頁面打開的不一樣時間獲取性能數據

/**
  * 執行頁面信息收集
  *
  * @param {PassContext} passContext
  */
async run(runOptions: RunOptions) {
  const gathererResults = {};
  // 使用 Puppeteer 建立無頭瀏覽器,建立頁面
  const passContext = await this.prepare(runOptions);
  try {
    // 根據用戶是否輸入了用戶名和密碼判斷是否要登陸政採雲
    await this.preLogin(passContext);
        // 頁面打開前的鉤子函數
    await this.beforePass(passContext);
        // 打開頁面,獲取頁面數據
    await this.getLhr(passContext);
        // 頁面打開後的鉤子函數
    await this.afterPass(passContext, gathererResults);
        // 收集頁面性能
    return await this.collectArtifact(passContext, gathererResults);
  } catch (error) {
    throw error;
  } finally {
    // 關閉頁面和無頭瀏覽器
    await this.disposeDriver(passContext);
  }
}

○ 建立無頭瀏覽器

建立無頭瀏覽器和頁面,並指定瀏覽器對應的寬高,指定運行的參數,關於瀏覽器的參數能夠參考以下文章:Puppeteer API。能夠將 headless 設置爲 false 看到瀏覽器的建立和 page 的新建,本地調試可使用。

/**
  * 登陸前準備工做,建立瀏覽器和頁面
  *
  * @param {RunOptions} runOptions
  */
async prepare(runOptions: RunOptions) {
  // puppeteer 啓動的配置項
  const launchOptions: puppeteer.LaunchOptions = {
    headless: true, // 是否無頭模式
    defaultViewport: { width: 1440, height: 960 }, // 指定打開頁面的寬高
    // 瀏覽器實例的參數配置,具體配置能夠參考此連接:https://peter.sh/experiments/chromium-command-line-switches/
    args: ['--no-sandbox', '--disable-dev-shm-usage'],
    executablePath: '/usr/bin/chromium-browser', // 默認 Chromium 執行的路徑,此路徑指的是服務器上 Chromium 安裝的位置
  };
  // 服務器上運行時使用服務器上獨立安裝的 Chromium
  // 本地運行的時候使用 node_modules 中的 Chromium
  if (process.env.NODE_ENV === 'development') {
    delete launchOptions.executablePath;
  }
  // 建立瀏覽器對象
  const browser = await puppeteer.launch(launchOptions);
  // 獲取瀏覽器對象的默認第一個標籤頁
  const page = (await browser.pages())[0];
  // 返回瀏覽器和頁面對象
  return { browser, page };
}

○ 模擬登陸

模擬登陸的場景能夠參考另外一篇,「百策系統」實現模擬登陸的實現,大體的實現邏輯以下:經過無頭瀏覽器打開政採雲登陸頁,經過 Puppeteer API 模擬輸入用戶名密碼,並模擬點擊登陸按鈕。根據同一瀏覽器下相同的域名共享 Cookie 的特性,再新開標籤頁打開須要檢測的 URL,即可以開始性能檢測。

○ 打開頁面

如何在 Puppeteer 中使用 Lighthouse 能夠參考Using Puppeteer with Lighthouse。下面的代碼主要檢測的是桌面端 Web 頁面的性能,後續會放開更改檢測環境的功能:能夠根據政採雲域名來判斷頁面是手機端仍是電腦端,根據不一樣的系統環境,切換不一樣的瀏覽器參數。

/**
  * 在 Puppeteer 中使用 Lighthouse
  *
  * @param {RunOptions} runOptions
  */
async getLhr(passContext: PassContext) {
  // 獲取瀏覽器對象和檢測連接
  const { browser, url } = passContext;
  // 開始檢測
  const { artifacts, lhr } = await lighthouse(url, {
    port: new URL(browser.wsEndpoint()).port,
    output: 'json',
    logLevel: 'info',
    emulatedFormFactor: 'desktop',
    throttling: {
      rttMs: 40,
      throughputKbps: 10 * 1024,
      cpuSlowdownMultiplier: 1,
      requestLatencyMs: 0, // 0 means unset
      downloadThroughputKbps: 0,
      uploadThroughputKbps: 0,
    },
    disableDeviceEmulation: true,
    onlyCategories: ['performance'], // 是否只檢測 performance
    // chromeFlags: ['--disable-mobile-emulation', '--disable-storage-reset'],
  });
  // 回填數據
  passContext.lhr = lhr;
  passContext.artifacts = artifacts;
}

○ 鉤子函數

鉤子函數實際是一個抽象類,在運行不一樣的 Gathering 時,對應的 Class 會實現該抽象類。鉤子函數的主要功能在於不一樣時期註冊回調,主要有 2 個鉤子函數,beforePass 和 afterPass。beforePass 的做用主要是在頁面還沒加載前先註冊一些監聽器,好比說想在頁面 load 以後,就拿到 DOM 節點的深度,那就須要在 beforePass 中註冊監聽。afterPass 主要是頁面性能統計完成以後,返回結構化的數據。

/**
  * 執行全部收集器中的 afterPass 方法
  *
  * @param {PassContext} passContext
  * @param {GathererResults} gathererResults
  */
async afterPass(passContext: PassContext, gathererResults: GathererResults) {
  const { page, gatherers } = passContext;
  // 遍歷全部收集器,執行 afterPass 方法
  for (const gatherer of gatherers) {
    const gathererResult = await gatherer.afterPass(passContext);
    gathererResults[gatherer.name] = gathererResult;
  }
  // 執行完全部方法後截圖記錄
  gathererResults.screenshotBuffer = await page.screenshot();
}

○ 收集器的實現

百策總共有 6 個收集器,分別是 Domstats Gathering,Image Elements Gathering,Lighthouse Gathering,Metrics Gathering, Network Recorder Gathering 和 Performance Gathering。

每一個收集器都會實現特定的收集功能:

  • Domstats Gathering: 收集 DOM 相關的數據,好比 DOM 元素數量,DOM 最大深度,document 是否有滾動條等。
  • Image Elements Gathering:收集全部的圖片,並記錄下圖片的寬高,定位等屬性。
  • Lighthouse Gathering:收集 Lighthouse 相關的指標:好比 FCP、LCP、TBT、CLS 等等。
  • Metrics Gathering:收集 JS 事件監聽數量,JS 堆棧大小等。
  • Network Recorder Gathering:收集全部頁面請求,包括狀態碼,請求方式,請求頭,響應頭等。
  • Performance Gathering:主要記錄了 window.performance 下的一些數據,用於計算一些時間。

以 Domstats Gathering 作爲例子,詳細說明如何獲取頁面檢測數據。首先實現抽象類的 2 個方法:beforePass 和 afterPass。beforePass 的實現邏輯是對 page 對象添加 domcontentloaded 時間點的監聽方法,監聽方法的主要功能是判斷 document 是否有橫向滾動條。afterPass 方法主要是獲取 Lighthouse lhr 中的數據,分析並獲得 DOM 最大深度,DOM 節點數等。

import { Gatherer } from './gatherer';
import { PassContext } from '../interfaces/pass-context.interface';
// 實現 Gatherer 抽象類
export default class DOMStats extends Gatherer {
  horizontalScrollBar;
  /**
  * 頁面打開前的鉤子函數
  *
  * @param {PassContext} passContext
  */
  async beforePass(passContext: PassContext) {
    const { browser } = passContext;
    // 當瀏覽器的對象發生變化的時候,說明新打開頁面了,此時能夠獲取到標籤頁 page 對象
    browser.on('targetchanged', async target => {
      const page = await target.page();
      // 等待 dom 文檔加載完成的時候
      page.on('domcontentloaded', async () => {
        // 經過 evaluate 方法能夠獲取到頁面上的元素和方法
        this.horizontalScrollBar = await page.evaluate(() => {
          return document.body.scrollWidth > document.body.clientWidth;
        });
      });
    });
  }
  /**
  * 頁面執行結束後的鉤子函數
  *
  * @param {PassContext} passContext
  */
  async afterPass(passContext: PassContext) {
    const { artifacts } = passContext;
        // 從 lighthouse 結果對象 lhr 中獲取 dom 節點的 depth,width 和 totalBodyElements
    const {
      DOMStats: { depth, width, totalBodyElements },
    } = artifacts;
    return {
      numElements: totalBodyElements,
      maxDepth: depth.max,
      maxWidth: width.max,
      hasHorizontalScrollBar: !!this.horizontalScrollBar,
    };
  }
}

等待全部 Gathering 都執行完成以後,數據就能夠落庫了。

○ 根據模型計算得分

數據入庫後還要根據不一樣的模型計算不一樣的得分。前臺頁面重展現,而且圖片加載會比較多,中臺頁面重表單提交,因此不一樣的模型必定有不一樣的計算邏輯。在政採雲,前臺頁面咱們使用的框架是 Vue, 中臺頁面使用的是 React(部分頁面因爲歷史緣由用的仍是 jQuery)。因此大體能夠根據框架來區分模型。判斷框架是 Vue 仍是 React 能夠根據 DOM 是否包含 _reactRootContainer__vue__ 來判斷。

/**
  * 計算得分方法,根據模型上的得分配置項最終生成得分併入庫
  *
  * @param {Artifact} artifact
  * @param {string[]} whitelist
  */
async calc(artifact: Artifact, whitelist?: string[]): Promise<AuditDto> {
  // 根據每條 metaid 動態加載不一樣的計算方法文件,每一個 metaid 指的就是一個性能評分指標,好比說是否有橫向滾動條
  const audit = await import(`../audits/${this.meta.id}`).then(m => m.default);
    // 執行每一個計算方法文件中的 audit 方法,計算得分,好比沒有橫向滾動條的時候得5分,有橫向滾動條不得分
  const { rawValue, score, displayValue, details = [] } = audit.audit(artifact, whitelist);
  const auditDto = new AuditDto();
  auditDto.id = this.meta.id;
    // 檢測指標名稱展現
  auditDto.title = this.meta.title;
    // 檢測指標描述
  auditDto.description = this.meta.description;
    // 檢測指標詳情
  auditDto.details = details;
    // 檢測指標登記,判斷是否計算入得分
  auditDto.level = this.level;
  // 扣分上限根據不一樣的 meta,可能上限也有不一樣,upperLimitScore 指的是扣分上限,從數據庫獲取
  auditDto.score = score * this.weight <= -this.upperLimitScore ? -this.upperLimitScore : score * this.weight;
    // 得分狀況
  auditDto.rawValue = rawValue;
    // 得分如何展現
  auditDto.displayValue = displayValue;
  return auditDto;
}

如下是政採雲前臺模型,每一項都是一個檢測指標,告警項只作提示,不實際扣分,前臺主要以圖片加載和展現爲準,因此模型設計上,會更加側重頁面加載時間的關鍵指標,而且會着重考慮圖片的展現。

前面內容主要介紹了百策的數據採集和評分功能,這也是百策最主要的功能。除了核心功能外,百策還有數據看版、提供性能解決方案、性能走勢,性能對比,定時監測等功能。在這篇文章中我也不一一闡述了。

○ 自動檢測

固然除了上面這些手動檢測之外,百策也支持自動檢測。自動檢測的主要目的是統計全部收錄在系統中的頁面,統計哪些頁面性能優化的最好,哪些優化欠佳。具體的邏輯:每週五 2 點會對全部收錄在百策中的頁面進行檢測,將檢測成績最高的 10 個頁面,檢測成績最低的 10 個頁面,檢測成績進步最快的 10 個頁面,自動檢測的邏輯主要經過 node-schedule 實現。發送郵件能夠 ejs 實現渲染模版,定義好模版後經過 nodemailer 發送便可。

import {
  Injectable,
  OnModuleInit,
} from '@nestjs/common';
import * as schedule from 'node-schedule';
@Injectable()
export class ScheduleService implements OnModuleInit {
  onModuleInit() {
    this.init();
  }
  async init() {
    // 本地啓動時不執行一系列定時任務
    if (process.env.NODE_ENV !== 'development') {
      // 每週五02:00開始收集頁面性能
      schedule.scheduleJob(`hawkeye-weekly-report`, '0 0 2 * * 5', async () => {
        // 調用檢測接口記錄性能評分
        await this.report();
      });
      // 每週五18:00發送週報
      schedule.scheduleJob(`hawkeye-weekly-send`, '0 0 18 * * 5', async () => {
        // 發送郵件的具體實現方法,主要經過 ejs 渲染模版,經過 nodemailer 發送郵件
        await this.send();
       });
    }
  }
}

○ 對接魯班

關於魯班是什麼,能夠參考這篇文章:前端工程實踐之可視化搭建系統,用一句話來總結,能夠說魯班就是政採雲的頁面搭建系統。

在對接魯班時,主要包括了魯班頁面的性能數據的錄入和魯班頁面的錄入(方便後續每週定時檢測)。

  • 魯班性能數據的錄入:和在魯班生成頁面時提供一個檢測按鈕,調用百策性能評分接口,生成檢測數據。
  • 魯班頁面的錄入:在魯班的新頁面上線的時候,會自動調用百策錄入接口,新增的頁面會被錄入到百策系統中。

結尾

若是你也想搭建一個屬於本身的性能檢測平臺,而且恰巧看到了這篇文章,但願此文對你有所幫助。

本文最主要講的是如何搭建一個性能平臺。當你已經可以搭建性能平臺以後,不妨能夠思考下業務頁面的檢測模型。

推薦閱讀

淺析 vue-router 源碼和動態路由權限分配

編寫高質量可維護的代碼:一目瞭然的註釋

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索