前端效能:股票交易APP頻繁更新怎麼破

Peter譚金傑發表於2020-06-11

寫本文的原因

  • 有幾位小夥伴最近又來問這個問題,之前幫人解答過一次,今天寫下來
  • 以後有時間會多寫一些解決方案,例如oom了,不用esbuild怎麼解決之類的等..

正式開始

主題:股票交易APP(IM場景前端互動高頻更新卡頓)

  • 一個正常的股票交易APP,是很複雜的,大都用原生寫,但是有的公司沒錢啊,只能做一套web app或者用RN這些寫,也有用Flutter的(這就是沒錢又要玩,那怎麼辦呢?那就玩乞丐版呀)

一個股票交易APP的介面長這樣

  • 首先金融交易類產品是IM產品的一種,大都使用私有基於TCP長連結私有協議或者wss協議,這裡推薦兩篇我之前寫的文章,這樣你來看本文效果會比較好。

    • 手寫實現一個websocket協議(基於Node.js)
    • 手寫一個React框架

問題重現

  • 使用者收藏了1000只自選股(國內國外+期貨+指數等),技術棧是web app ,基於react或React-native,很卡頓
  • 由於是雙工通訊,而且高頻推送,觸發更新,而且交易類APP對訊息送達的效率/低延遲要求非常高,例如你準備買這隻股票,此時大戶砸盤,你還沒收到更新的資訊,下單,發現趨勢已經走壞,然後接盤被套。
  • 還有一種情況,你買入的時候出了大利好,你下單價格是10塊錢,但是此時已經漲到10.05,這個價格成成交不了,然後你錯過了一波大漲。這時候客戶就慘了

需求簡單&技術的剖析

  • 理論上使用者可以新增的自選股票,是無限的
  • 每個自選股/股票的都有對應的事件觸發

  • 高頻更新,此時要區分react/react-native環境,因為react-native元件在掛載後就不會解除安裝了,不像web app.

原則

  • 效能優化最好是簡單的手段
  • 所見即所得,簡單高校,不觸碰底層邏輯,例如網路層前後端可能都要做粘包的處理
  • ...不做可能誘發P0級別事故的技術方向選擇

解決問題

  • react/react-native渲染上有區別,對於長列表,react-native是有元件對應只渲染可視區域,react則不會,需要虛擬列表,推薦react-peter-window這個庫,而且可以支援自動高寬

原始碼demo地址:https://github.com/JinJieTan/react-keepAlive-dynamic

  • 這樣react也可以跟react-native的元件一樣,只渲染可視區域了
  • 長列表問題解決了,但是事件同時也很麻煩,理論上使用者可以新增無限的自選股,這個列表可能就有無限長(不要說不可能,世界在發展,這就是高可用的APP),傳統的事件需要每個item去繫結,然後切換元件時候再remove掉,但是頻繁對事件掛載、移除其實也很損耗效能,這裡換成事件冒泡,就可以了,把需要的資料掛載到dom的屬性上獲取即可~
  • 上面說的,不要小看,能解決相當一部分效能問題

最重要的高頻更新的問題

  • 不同金融交易類公司,後端架構設計不一樣,訊息推送也是,例如大智慧的後端架構就比較特殊.
  • 前端網路層可能要處理粘包,後端的訊息推送頻率我們不管
  • 借鑑PReact、Redis、kafka的思想,自己在前端實現一個訊息佇列,定期消費,更新介面.
  • 參考我之前手寫React的程式碼:

`https://github.com/JinJieTan/mini-react/tree/hooks

import { _render } from '../reactDom/index';

import { enqueueSetState } from './setState';

export class Component {

  constuctor(props = {}) {
    this.state = {};
    this.props = props;
  }
  
  setState(stateChange) {
    const newState = Object.assign(this.state || {}, stateChange);
    console.log(newState,'newState')
    this.newState = newState;
    enqueueSetState(newState, this);
  }
  
}` 
  • 當setState後,先進入佇列中,首次進入,佇列為空,進入判斷,下一幀渲染前呼叫defer(flush)
`export function enqueueSetState(stateChange, component) {

  //第一次進來肯定會先呼叫defer函式
  if (setStateQueue.length === 0) {
    //清空佇列的辦法是非同步執行,下面都是同步執行的一些計算
    defer(flush);
  }

  //向佇列中新增物件 key:stateChange value:component
  setStateQueue.push({
    stateChange,
    component,
  });

  //如果渲染佇列中沒有這個元件 那麼新增進去
  if (!renderQueue.some((item) => item === component)) {
    renderQueue.push(component);
  }
}` 
  • defer函式
`function defer(fn) {
  //高優先順序任務 非同步的 先掛起
  return requestAnimationFrame(fn);
}` 
  • 此時訊息再次推送,再次觸發enqueueSetState,資料此時被推送到佇列中,一幀統一合併消費。

其實瀏覽器也是有渲染佇列的,例如你在一個for迴圈裡面頻繁操作dom,並不會每次操作dom都會導致瀏覽器渲染,達到一個閥值,就會觸發渲染,當然你也可以手動控制清空佇列(這裡不寫太深,有興趣的可以關注後面的文章)

寫在最後

  • 我是Peter,架構設計過20萬人端到端加密超級群功能的桌面IM軟體,現在是一名前端架構師。
  • 如果你對效能優化有很深的研究,可以跟我一起交流交流,今天這裡寫得比較淺,但是大部分人都夠用,之前問我的朋友,我讓它寫了一個定時器定時消費佇列,最後也能用。哈哈
  • 另外歡迎收藏我的資料網站:前端生活社群:https://qianduan.life
  • 感覺對你有幫助,可以右下角點個在看,關注一波公眾號:[前端巔峰]

相關文章