1.消息列表引起老师端卡死白板白屏
这篇文章是血的教训,罚款500大洋换来的~挺好,说明自己还有发展的空间,解决并发问题有所欠缺,经验不够。
事情的缘由:
自己目前在公司负责一款并发使用场景很大的产品,对客户端的性能是有一定要求的。
事情发生的原因:
老师在使用客户端给学生们讲课,在与学生互动时,送花和聊天消息巨多的时候,消息列表的DOM频繁渲染,且数量未得到有效控制,导致客户端卡死白屏。
事情解决方案:
1.使用虚拟列表List,不是来一条消息,生成一个DOM(列表DOM是固定的)
2.使用带性能的数组List,节省内存
3.数组限制最新500条,减少内存消耗
4.关闭console.log,避免内存泄漏
5.使用React.memo控制列表渲染频率,避免无效渲染
6.使用useMemo控制数组List,减少性能消耗
7.使用节流throttle,降低渲染频率,1s渲染只一次
2.具体实施方案
1.使用虚拟列表
使用第三方库:rc-virtual-list
github地址:https://github.com/react-component/virtual-list
使用前:
当收到一条聊天消息,就会生成一个div,总共1500多个div生成出来,DOM只会越来越多。
使用后:
只会生成可视区域的几个div,然后通过css样式实现列表下拉效果。
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from "react"; import VirtualList from 'rc-virtual-list'; import { Message } from './message';
const ChatList: React.FC<any> = ({ messages }) => { return ( <VirtualList itemHeight={80} itemKey="ts" data={messages} height={ document.body.clientHeight - 250 } children={(item, index) => <Message key={index} {...item}/> } /> ); }; export default ChatList;
|
注意:必须设置itemHeight和height,itemHeight是一条消息的最小高度,height是可视区高度,否则无效。
2.使用带性能的数组List
使用第三方库:immutable.js
该库同属于facebook团队开源出来的,经典的react.js也是出于他们团队。
react地址:https://github.com/facebook/react
immutable地址:https://github.com/facebook/immutable-js/
Imutualble概念:顾名思义,对象一旦被创建便不能更改
,对immutable对象的修改添加删除都会返回一个新的immutable对象,同时为了避免deepCopy的性能损耗,immutable引入了Structural Sharing(结构共享),如果对象只是一个节点发生变化,只修改这个节点和受它影响的父节点,其他节点共享。
使用immutable,可以优化下性能:
举例:
这里有100,000条聊天消息:
1 2 3 4 5 6 7 8 9 10 11 12 13
| var msgs = { ⋮ t79444dae: { msg: 'Task 50001', completed: false }, t7eaf70c3: { msg: 'Task 50002', completed: false }, t2fd2ffa0: { msg: 'Task 50003', completed: false }, t6321775c: { msg: 'Task 50004', completed: false }, t2148bf88: { msg: 'Task 50005', completed: false }, t9e37b9b6: { msg: 'Task 50006', completed: false }, tb5b1b6ae: { msg: 'Task 50007', completed: false }, tfe88b26d: { msg: 'Task 50008', completed: false }, ⋮ (100,000 items) }
|
我要把第50,005条聊天消息的completed改为ture。
用普通的JavaScript对象:
1 2 3 4 5 6 7 8 9
| unction toggleTodo (todos, id) { return Object.assign({ }, todos, { [id]: Object.assign({ }, todos[id], { completed: !todos[id].completed }) }) }
var nextState = toggleTodo(todos, 't2148bf88')
|
这项操作运行了134ms
。
为什么用了这么长时间呢?
因为当使用Object.assign,JavaScript会从旧对象(浅)复制每个属性到新的对象。
我们有100,000条聊天消息,就意味着有100,000个属性需要被(浅)复制。
这就是为什么花了这么长时间的原因。
在JavaScript中,对象默认是可变的。
当你复制一个对象时,JavaScript不得不复制每一个属性来保证这两个对象相互独立。
使用Immutable.js
1 2 3 4 5 6 7 8 9 10
| function toggleTodo (todos, id) { return u({ [id]: { completed: (completed) => !completed } }, todos) }
var nextState = toggleTodo(todos, 't2148bf88')
|
这项操作运行了1.2ms。速度提升了100倍。
为什么会这么快呢?
可持久化的数据结构强制约束所有的操作,将返回新版本数据结构,并且保持原有的数据结构不变,而不是直接修改原来的数据结构。
这意味着所有的可持久化数据结构是不可变的。
鉴于这个约束,第三方库immutable.js在应用可持久化数据结构后可以更好的优化性能。
最后一句话总结:使用immutable定义的数组和对象,在react render渲染的时候,可以实现结构共享、DOM共享。
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { List } from 'immutable'; import { ChatMessage } from '../../utils/types';
interface ChatPanelProps { messages: List<ChatMessage> value: string sendMessage: (evt: any) => void handleChange: (evt: any) => void }
const ChatPanel: React.FC<ChatPanelProps> = ({ messages, value, sendMessage, handleChange, }) => { return ( <ChatList messages={messages}/> ) }
|
3.数组限制最新500条
消息列表定义了一个messages,按理messages说只是一个变量,当messages.push(xxx)执行10000000….次后,该变量会越来越大,压测或并发大的时候,肯定会引起内存的增大。
简单粗暴的解决方式是,只取最新的500条消息放入该变量中
。
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12
| updateChannelMessage(msg: ChatMessage) { let { messages } = this.state messages = messages.push(msg) if (messages.size >= 500) { messages = messages.slice(-500) } this.state = { ...this.state, messages }; this.commit(this.state); }
|
注意:该message使用的是immutable.js的List,对应的用法和传统JS数组不同。
4.关闭console.log,避免内存泄漏
之前在接收/发送学生消息的时候,都会打印消息类型、内容及消息人,一有学生进入进出,一有学生举手送花,一有学生发送聊天消息,老师这边都会打印日志,而且是频繁打印。
打印日志,之前以为不会占用内存。但是取消打印日志后,内存竟然真的降了。
查阅相关资料后,发现不停的打印日志,确实会导致内存增加。
原因是因为传递给console.log的对象不能被垃圾回收
。
5.使用React.memo控制列表渲染频率,避免无效渲染
此话怎么解释呢,就拿聊天区举例,聊天区有两个核心组件:
一、输入框发送聊天消息组件
二、聊天消息列表组件
聊天父组件A,拥有输入框子组件B和列表子组件C,父组件A里有聊天消息数组messages和老师输入的内容value。
当老师发送聊天消息时,子组件B的input输入框的value值会发生变化,从而引发父组件A的渲染。
但是问题来了,父组件A有子组件B和C,父组件一渲染,会引起子组件C的再次渲染,尽管子组件C什么都没动!
说的可能有些绕,直接看图:
聊天组件:ChatPanel
输入框组件:ChatTool
列表组件:ChatList
老师在聊天输入框中,每输入一个字符,就会引起聊天消息列表的再次渲染,这是很可怕的!消息明明都还没有点击“发送”,输入的值都还没传到消息列表去,就一直渲染。
遇到这种情况,React.memo的神奇之处就来了。
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from "react"; import VirtualList from 'rc-virtual-list'; import { Message } from './message';
const ChatList: React.FC<any> = ({ messages }) => { return ( <VirtualList itemHeight={80} itemKey="ts" data={messages} height={ document.body.clientHeight - 250 } children={(item, index) => <Message key={index} {...item}/> } /> ); }; export default React.memo(ChatList);
|
注意:之前ChatList组件是export default ChatList
,现在ChatList组件是export default React.memo(ChatList)
。
React.memo是当组件props传来的值发生变化才会触发渲染,没有发生变化则不会触发渲染
优化后的效果:
可以发现列表组件ChatList只在初始化页面的时候,被渲染一次,输入字符后不会引起列表组件ChatList再次渲染。
6.使用useMemo控制数组List,减少性能消耗
React.memo作用于组件的渲染是否重复执行,同理,我们可以控制变量的计算useMemo,函数的逻辑useCallback是否重复执行。
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React, { useMemo } from 'react'; import { ChatMessage } from '../../utils/types';
interface ChatPanelProps { messages: List<ChatMessage> value: string sendMessage: (evt: any) => void handleChange: (evt: any) => void }
const ChatPanel: React.FC<ChatPanelProps> = ({ messages, value, sendMessage, handleChange, }) => { const messageList = useMemo( () => { return messages.toJSON() },[messages]) return ( <ChatList messages={messageList}/> ) }
|
7.使用节流throttle,降低渲染频率,1s只渲染一次
函数节流(throttle):高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率
。
说完函数节流,再说一说函数防抖(debounce):触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
。
函数节流(throttle)与 函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象
。
实现方法:
1.新增一个临时变量,专门用于存放聊天消息列表的数组;
另一个变量,专门用于渲染聊天消息列表的数组。
1 2 3 4 5 6
| export type RoomState = { // 渲染的数组 messages: List<ChatMessage> // 存放的数组 tempMessage: List<ChatMessage> }
|
2.在老师发送消息及接收学生消息的地方,使用节流去控制渲染频率。
思路分析:
1.收到或发送多条消息,直接存到tempMessage,但不发生页面渲染。
2.控制每1s只渲染一次,即把tempMessage赋值给messages,发生页面渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import { throttle } from 'lodash';
const push = throttle(function() { roomStore.updateMessages(); }, 1000)
const sendMessage = (content: string) => { const message = { account: me.account, id: me.uid, headImg: me.headImg, role: `${me.role}`, text: content, ts: +Date.now() } roomStore.updateChannelTempMessage(message); push() }
rtmClient.on("ChannelMessage", ({ message }: { message: { text: string } }) => { if (cmd === ChatCmdType.chat) { const message = { headImg: p.headImg, account: p.userName, role: p.role, text: data, ts: +Date.now(), id: fromUserId, } roomStore.updateChannelTempMessage(chatMessage) push() } })
|
updateChannelTempMessage存放和updateMessages渲染方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| // 渲染的数组,触发渲染 updateMessages() { this.state = { ...this.state, messages: this.state.tempMessage, } this.commit(this.state); } // 存放的数组 updateChannelTempMessage(msg: ChatMessage) { let { tempMessage } = this.state tempMessage = tempMessage.push(msg) if (tempMessage.size >= 500) { tempMessage = tempMessage.slice(-500) } this.state = { ...this.state, tempMessage, }; this.commit(this.state); }
|
3.总结
前端是门学无止境的技术,入门容易,精通难~
对于初学者而言,不就是写页面,html、css、js一套,so简单。
其实不然,每门编程语言的诞生都有它存在的理由,只是说前端技术变化真的太快,react17和vue3的到来,相信又会淘汰一批前端老人吧。