写本文的原因
- 有几位小伙伴最近又来问这个问题,之前帮人解答过一次,今天写下来
- 以后有时间会多写一些解决方案,例如 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
- 感觉对你有帮助,可以
右下角
点个在看
,关注一波公众号:[前端巅峰]