騰訊課堂小程序性能極致優化——網絡請求優化篇


 

騰訊課堂小程序性能極致優化——網絡請求優化篇 https://mp.weixin.qq.com/s/g2mLpWhGsrMEud-i8xD6YQ

zhuojun 騰訊IMWeb前端團隊 2021-07-22

 

近期,我們對騰訊課堂小程序做了一次全方位的性能優化,本篇文章將從網絡請求的角度分享一種優化的思路。我們引入了一種請求排隊的策略,通過控制不同優先級請求的發送順序,保障影響頁面渲染的關鍵請求能夠及時發送,並迅速得到返回結果。由請求測速數據統計,我們的關鍵請求耗時實現了 50-100 ms,約 15% 的優化。

1. 導火索 🔥

某個平淡無奇的工作日,我們的小程序在 iOS 上突然無比的卡頓。

緊急排查之后,發現我們使用的一個第三方上報服務異常,導致我們的上報請求一直處於 pending 狀態。

這時,小程序官方文檔的網絡使用說明映入眼簾:

wx.request、wx.uploadFile、wx.downloadFile 的最大並發限制是 10 個。

也就是說,由於並發限制,當處於 pending 狀態的請求數達到 10 個時,后續調用 wx.request 發送的請求可能會被阻塞。

時間較為久遠,沒有留下證物,我們通過模擬請求超時來還原一下當時的現場。

借助 whistle 的 resDelay 方法,可以將 report 和 log 兩種上報請求延遲 5000ms 返回:

 

 

可以看到由於上報請求一直處於 pending 狀態,導致后續發送的業務請求 get_homepage_feeds_h5 也一直在 pending 中,頁面加載變得卡頓起來。

 

 

排查到問題后,我們緊急下掉了異常請求,現網突發的卡頓問題得以解決。

但是,這次的異常引起了大家的注意:上報請求竟然會對業務請求造成如此大的影響。我們似乎應該通過一些方式,保障與用戶體驗相關的業務請求正常發送

2. 設想 🔍

網絡請求的耗時與許多因素相關,用戶的網絡環境以及提供接口的服務質量,我們無法在前端控制。

但如果我們可以給小程序的網絡請求設置優先級,當多個請求並發時,讓低優先級的上報請求給高優先級的業務請求讓路,是否也能讓業務請求速度提升,優化用戶體驗呢?

 

 

3. 方案實現 📃

我們設計的請求優先策略如下:

  1. 將請求分為高優先與低優先兩種等級。
  2. 當並發請求數超過一定閾值時,僅發送高優先級請求,攔截低優先級請求的發送。
  3. 並發請求數量下降后,補發被攔截的低優先級請求。
  4. 設置最長等待時間,超時后主動發送低優先請求,避免過度延時。

小程序的 HTTPS 請求是通過 wx.request API 發送的,我們可以通過攔截這個 API 來實現對所有請求的發送順序控制。整塊邏輯我們封裝到一個 排隊請求模塊 來實現,模塊中提供一個與 wx.request 具有相同函數簽名的方法,用來替換原始的請求 API。

3.1. 關鍵配置

threshold: 並發請求數的閾值。當正在進行的請求數超過這個閾值時,延遲發送低優先請求。

maxWaitingTime:  最長等待時間。等待隊列中的請求等待超過該時間后,主動補發,避免過度延時。

lowPriority: 一組匹配方式(正則等)。用於判斷請求是否屬於低優先級。

目前我們設置 threshold 為 5,主要出於以下兩方面考慮:

  1. 給后續可能到來的業務請求預留 5 個坑位,避免阻塞。
  2. 已有 5 個請求並發時,延遲上報請求發送,也可減少並發造成的網絡爭搶。

3.2. 初步設計

模塊基本功能由 3 個類實現:

  1. 請求分發器 QueueRequest:對新的請求進行分發。

    • 加入等待隊列:正在進行的請求數超過設置的 threshold,且請求為低優先級時;

    • 加入請求池:請求為高優先級,或並發數未達到 threshold

  2. 等待隊列 WaitingQueue:維護需要延時發送的請求等待隊列。在請求池空閑或請求超過最長等待時間時,補發等待請求。

  3. 請求池 RequestPool:發送請求並維護所有正在進行的請求的狀態。對外暴露正在進行的請求數量,並在有請求完成時通知等待隊列嘗試補發。

 

 

這里簡單通過各個類的接口來描述一下類之間的包含、調用關系:

QueueRequest 類

class QueueRequest {
  
  // 請求池
  private requestPool: RequestPool;
 
  // 等待隊列
  private waitingQueue: WaitingQueue;
  
  // 用於替代wx.request的請求方法
  request(opts: WechatMiniprogram.RequestOption): WechatMiniprogram.RequestTask;
}

WaitingQueue 類

class WaitingQueue {
  
  // 等待發送的請求隊列
  private queue: QueueRequestOption[];
  
  // 檢查隊列是否有超過最大等待時長的請求
  private checkQueue();

  // 將一個參數為opt的新請求加入等待隊列
  public enqueue(opts: QueueRequestOption);

  // 發送等待隊列的第一個的請求
  public dequeue();

  // 獲取等待隊列的長度
  public getWaitingNum():number;
}

RequestPool 類

interface RequestPoolConfig {
  onReqComplete?: () => void;
}

class RequestPool {
  
  // 真實的wx.request方法
  private originRequest = wx.request.bind(wx);

  // 請求池,記錄正在進行中的請求
  private pool;

  // 將一個參數為opts的請求加入請求池並發送
  public add(opts: QueueRequestOption);

  // 請求完成時,將標識為seq的請求移出請求池,並觸發onReqComplete事件
  private remove(seq: number);

  // 獲取請求池內請求數量
  public getReqNum():number;
}

QueueRequest中維護了 WaitingQueue 和 RequestPool 兩個類的實例。

通過 WaitingQueue.enqueue() 將請求加入等待隊列;

通過 ReuqestPool.add() 將請求放入請求池,直接發送;

QueueRequest.request()方法用於替代 wx.request,在實際調用 wx.request 前執行請求分發邏輯。

此時,我們已經可以通過 QueueRequest.request() 方法,控制不同優先級請求的發送順序了。

3.3. RequestTask維護

然而,根據 API 文檔,調用 wx.request 會返回一個 RequestTask 對象。那么進入等待隊列的請求,在沒有調用 wx.request 時,要如何同步地返回該對象呢?

我們設計了 RequestTaskProxy 類來模擬真實的 RequestTask。

  1. 在請求未實際發送前先返回該對象代理,其實現了 RequestTask 的接口。
  2. 記錄下各個接口的調用參數,保存到內部的 operations 隊列中。
  3. 在請求實際發送后,將 operations 調用記錄重放到真實的 RequestTask。

增加了 RequestTaskProxy 后,新的請求進入排隊請求模塊,形成了如下的調用鏈路:

 

 

RequestTaskProxy 類的大致實現如下:

export class RequestTaskProxy implements WechatMiniprogram.RequestTask {
  
  // 真實的RequestTask
  private task?: WechatMiniprogram.RequestTask;
  
  // 未綁定真實RequestTask時的操作記錄
  private operations: [];
  
  /**
   * 設置真正發起請求后返回的requestTask
   * @param requestTask 實際wx.request返回對requestTask
   */
  public setRequestTask(requestTask: WechatMiniprogram.RequestTask) {
    this.task = requestTask;
    this.operations.forEach((op) => {
      // 將this.operations重放到requestTask上
      // ...
    });
    this.operations = [];
  }

  // 模擬RequestTask的接口
  public abort() {
    if (this.task) {
      this.task.abort();
    } else {
      this.operations.push({ type: 'abort' });
    }
  }
  // 模擬RequestTask的接口
  public offHeadersReceived(callback?: WechatMiniprogram.OffHeadersReceivedCallback) {
    if (this.task) {
      this.task.offHeadersReceived(callback);
    } else {
      this.operations.push({ type: 'off', cb: callback });
    }
  }
  // 模擬RequestTask的接口
  public onHeadersReceived(callback: WechatMiniprogram.OnHeadersReceivedCallback) {
    // 類似offHeadersReceived
    // ...
  }
}

3.4. 替換 wx.request

由於 wx.request 屬性的 writable 被設置為 false,不能直接賦值,我們只能通過 Object.defineProperty 替換該屬性的 value 值。

class QueueRequest {
  request(opts: WechatMiniprogram.RequestOption): RequestTask;
  
  // 替換wx.request方法
  make() {
    Object.defineProperty(wx, 'request', { value: this.request.bind(this) });
  }
}

/**
 * 對外暴露的方法,調用后注入排隊請求邏輯
 * @param config 配置項
 */
export function useRequestQueue(config: QueueRequestConfig) {
  const queueRequest = new QueueRequest(config);
  queueRequest.make();
  return queueRequest;
}

在小程序中,調用 useRequestQueue() 方法,即可攔截 wx.request API,按照配置的優先級控制請求順序了。

4. 優化效果 📈

在后台服務沒有任何改動的情況下,我們在前端干預了部分用戶網絡請求的順序,並上報統計了從發起業務請求獲得請求結果的耗時。

該耗時的計算近似於請求 API 觸發 complete 回調的時間 - 調用注入后的 wx.request 的時間

我們隨機選擇了一批用戶進行灰度,經過一段時間后,得到了對比數據:

4.1. 請求平均耗時對比

 

 

注:圖表的縱軸為用戶 業務請求(高優先) 的平均耗時,橫軸為時間軸

綠色曲線來自不做干預的普通用戶

黃色曲線來自應用請求優先策略的灰度用戶

從圖中可以看到,對網絡請求順序的干預效果明顯,灰度用戶業務請求耗時平均有 50-100ms,約 15% 的優化

4.2. 不同耗時請求的優化效果對比

 

 

注:圖表的縱軸為用戶 業務請求(高優先) 耗時,橫軸為時間軸

曲線大致可分為三組,每組兩條曲線,選取不同百分位的請求耗時對比,從上到下依次為:

  1. 請求耗時的 80 分位
  2. 請求耗時的 50 分位(中位數)
  3. 請求耗時的 20 分位

我們發現:對網絡請求順序的干預在耗時長的網絡請求中效果更為明顯。也就是說,這次的優化效果主要作用在弱網用戶上

該表現是符合預期的,因為在弱網環境下,更容易發生請求堆積,對業務請求造成阻塞。

 


 

這是騰訊課堂小程序優化的第二篇專項優化文章,前一篇《騰訊課堂小程序性能極致優化——綜合篇》已收獲滿滿好評,后面我們還有【獨立分包與性能測試】這個專項優化,敬請期待。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM