一个烧cpu的前端bugBug缘起 我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ct
Bug缘起
我还记得那是在一年前,在一个平平无奇的下午,领导安排我改改表格字段,轻轻松松嘛。ctrl+v,ctrl+s保存一气呵成,正查看着效果,突然感受到手中的二手MacBook发烫了起来,风扇也开始呼呼的转,我之前一度以为这电脑是没风扇的,直到那天,才第一次感受到了风扇。于是我打开柠檬管家,好家伙,温度直达90°C,而我开发的网页也越来越卡,后面直接崩了!


Bug分析
当时发现这个Bug都惊呆了,这么严重,怎么线上没人反馈。赶紧联系测试,让他们也试试,结果他们没人复现,那估计就是我本地开发问题了。但是我后面也没办法百分百复现,没搞懂出现的逻辑。
是否中挖矿病毒?
之前公司服务器遇到过挖矿病毒,一般挖矿病毒是一直运行的,我这个只是开发的时候运行,而且查看任务管理器没发现奇怪进程,排除掉。

内存泄露排查
在网上看了下如何排查内存泄露,打开Memory,Performance等面板观察,结果因为出现的时候太卡,直接卡崩了,也没办法对比了。
灵感转机
当时想到会不会是第三方插件引起的,使用排查法,一个一个排查浏览器插件,最后发现是它:vue.js devtools。后面处理的办法也很简单,直接不用它就完事了,用beta版的暂时解决了,但是对于BUG出现的原因以及解决办法还是没有找到。
无心插柳
最近工作不忙,又想起这事来,于是准备再碰碰运气。在开发者工具使用以下代码捕获了message事件,发现控制台一直在输出,ok,那看来引起的原因就是它了。
window.addEventListener('message', e => console.log(e))
展开Event对象,发现是它干的。
source: 'vue-devtools-backend-injection',
payload: 'listening'
代码追踪
直接找到vue-devtools的仓库,克隆下来搜索关键字,找到如下关键代码。以下代码初看很简洁,细看有点懵。先监听了一个message,后面又触发,触发的回调还是触发事件,这不就直接死循环了嘛。后面问了下GPT,哦,它其实在满足某个条件的时候,会移除监听,所以正常不会死循环。
// backend.js
window.addEventListener('message', handshake)
function sendListening() {
window.postMessage({
source: 'vue-devtools-backend-injection',
payload: 'listening',
}, '*')
}
sendListening()
function handshake(e) {
if (e.data.source === 'vue-devtools-proxy' && e.data.payload === 'init') {
window.removeEventListener('message', handshake)
let listeners = []
const bridge = new Bridge({
listen(fn) {
const listener = (evt) => {
if (evt.data.source === 'vue-devtools-proxy' && evt.data.payload) {
fn(evt.data.payload)
}
}
window.addEventListener('message', listener)
listeners.push(listener)
},
send(data) {
// if (process.env.NODE_ENV !== 'production') {
// console.log('[chrome] backend -> devtools', data)
// }
window.postMessage({
source: 'vue-devtools-backend',
payload: data,
}, '*')
},
})
bridge.on('shutdown', () => {
listeners.forEach((l) => {
window.removeEventListener('message', l)
})
listeners = []
window.addEventListener('message', handshake) // 关键是它
})
initBackend(bridge)
}
else {
sendListening()
}
}
移除监听的地方
在proxy.js这里会监听到上面触发的事件,然后通知上面init初始化,移除掉handshake的监听。
// proxy.js
window.addEventListener('message', sendMessageToDevtools)
function sendMessageToDevtools(e) {
if (e.data && e.data.source === 'vue-devtools-backend') {
port.postMessage(e.data.payload)
}
else if (e.data && e.data.source === 'vue-devtools-backend-injection') {
if (e.data.payload === 'listening') {
sendMessageToBackend('init')
}
}
}
function sendMessageToBackend(payload) {
window.postMessage({
source: 'vue-devtools-proxy',
payload,
}, '*')
}
初见端倪
通过不断加debug发现,把开发工具关闭后,会触发onDisconnect,然后proxy.js会移除上面的事件监听。backend会进入bridge的‘shutdown’的回调,也就是这是这里把前面初始化时已经移除的监听又给加上了,导致后面出现触发message事件时,死循环了。
总结复现步骤
- 准备好一个vue页面,会定时更新组件的或者更新组件树的,比如轮播图页面。
- 打开vue开发者工具。
- 打开浏览器的任务管理器,cpu倒序展示。
- 最后关闭vue开发者工具。
- cpu上升到100%。
我的环境
- MacBook Pro (13-inch, M1, 2020)
- macOS Monterey 12.5.1
- Chrome 126.0.6478.185 (arm64)。
- 火狐要手动触发更新事件,也能复现。
复现代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<title>test</title>
<style>
.el-carousel__item h3 {
color: #475669;
font-size: 18px;
opacity: 0.75;
line-height: 300px;
margin: 0;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n+1) {
background-color: #d3dce6;
}
</style>
</head>
<body>
<div id="app">
<el-carousel indicator-position="outside">
<el-carousel-item v-for="item in 4" :key="item">
<h3>{{ item }}</h3>
</el-carousel-item>
</el-carousel>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script type="text/javascript">
new Vue({
el: '#app'
})
</script>
</body>
</html>
后续排查
-
在Git上排查加那行代码的原因,发现是2年前加上的,修复自动重连的BUG。 Commit。真想直接把那行代码干掉,搞得我的M1芯片红温了好几次。
-
本来准备提个ISSUE,结果发现已经有很多老哥提过了,并且还有个老哥3月提出了PR,但是没人理,难搞。下次开发的话,不要关闭devtools就不会出现了这个问题了。
转载自:https://juejin.cn/post/7402204094449188915