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样式实现列表下拉效果。
实现方法:
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条聊天消息:
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对象:
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
// 使用[updeep](https://github.com/substantial/updeep)
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共享。
实现方法:
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条消息放入该变量中
。
实现方法:
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的神奇之处就来了。 实现方法:
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是否重复执行。
实现方法:
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.新增一个临时变量,专门用于存放聊天消息列表的数组; 另一个变量,专门用于渲染聊天消息列表的数组。
export type RoomState = {
// 渲染的数组
messages: List<ChatMessage>
// 存放的数组
tempMessage: List<ChatMessage>
}
2.在老师发送消息及接收学生消息的地方,使用节流去控制渲染频率。 思路分析: 1.收到或发送多条消息,直接存到tempMessage,但不发生页面渲染。 2.控制每1s只渲染一次,即把tempMessage赋值给messages,发生页面渲染。
import { throttle } from 'lodash';
// 节流关键函数
const push = throttle(function() {
// 1s只执行一次,触发渲染
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渲染方法:
// 渲染的数组,触发渲染
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的到来,相信又会淘汰一批前端老人吧。