Open xiaoxiaojx opened 1 year ago
M 同学反映自己负责的 Next.js 项目疑似内存泄漏, 临近 618 需要尽快解决! 通过查看 Easy-Monitor 上的「堆内存趋势」曲线📈在一直上涨且不会下降就基本确定了是内存泄漏
M 同学也迅速定位到了造成内存泄漏的 commit, 我仔细 review 了一下并没有发现全局变量缓存、闭包引用等高危操作
💡 下载的两个快照要是同一个实例的同一个进程
接着就只能从 Easy-Monitor 上间隔一段时间前后下载了两个堆快照,最后通过 Chrome Devtool Memory 面板的 Comparison 功能进行对比, 发现 StyleRule 对象净新增了 57042 个 ⚠️ !
StyleRule
这里的一个小技巧是不要总盯着碎片化的(array)、Object、(string)以及系统的(system)、system / Context 等对象的内存变化, 这些对象既不好定位又不容易看懂, 它们通常只是某个对象的属性值, 受其他对象的泄漏而增长可能性大 所有我们需要优先关注 App 应用中使用到的对象, 比如上图中只在该项目中出现的 StyleRule 对象
这里的一个小技巧是不要总盯着碎片化的(array)、Object、(string)以及系统的(system)、system / Context 等对象的内存变化, 这些对象既不好定位又不容易看懂, 它们通常只是某个对象的属性值, 受其他对象的泄漏而增长可能性大
(array)
Object
(string)
(system)
system / Context
所有我们需要优先关注 App 应用中使用到的对象, 比如上图中只在该项目中出现的 StyleRule 对象
Chrome Devtool Memory 面板冗余信息多、可读性也较差, 推荐大家使用开源的 devtoolx 进行下一步的分析
npm install devtoolx -g devtoolx -s <heapsnapshot file> [-p <port>]
尴尬的是开始跑 devtoolx 命令时遇见了下面的报错 好吧, 我还是使用 lldb 先定位 devtoolx 启动失败的问题, 结果发现通过 lldb 启动 devtoolx 又能够正常跑起来
此时排除了 devtoolx 不能识别该 v8 版本的 .heapsnapshot 文件以及系统调用 api 兼容性问题(松了口气, 还是能用 devtoolx ~)
lldb -- /usr/local/bin/node /usr/local/bin/devtoolx -s /Users/duoxiaokai/Downloads/u-b259269e-6bd4-4336-8fc6-f04478496a47-u-x-heapdump-27-20230606-738634.heapsnapshot
看了一下 devtoolx 的代码, 猜想可能是打开 .heapsnapshot 文件失败了, 于是增加了如下代码再编译运行日志显示 ParseError: Operation not permitted
ParseError: Operation not permitted
std::ifstream jsonfile(parser->filename_); + if (!jsonfile.is_open()) { + std::cout << "\nfailed to open " << parser->filename_ << '\n'; + std::cerr << "ParseError: " << strerror(errno); + std::exit(1); + return; + } json profile; jsonfile >> profile;
所以把 .heapsnapshot 文件从 Downloads 目录移了出来就愉快的跑了起来, 上面的代码也提交了一个 devtoolx/pull/18/, 最后作者发布了 devtoolx@1.0.2 版本 ❤️
devtoolx@1.0.2
回归正题, 通过 devtoolx 分别对两个快照分析发现了
Object(674385)
StyleSheet.RuleList.xxx.StyleRule
此时我们可以看看 Chrome Devtool Memory 面板的 Summary 功能查看是否有更多 Object(674385) 对象的信息, 最终确认了 StyleRule 对象的引用关系是 StyleSheet.RuleList.map.StyleRule 接着使用 Chrome Devtool Memory 面板的 Comparison 功能查看发现 StyleRule 的父对象 StyleSheet 与 RuleList 并没有新增
StyleSheet.RuleList.map.StyleRule
StyleSheet
RuleList
根据引用关系定位到了 npm 包 jss 的代码, 我们缩小范围直击 RuleList 对象在何种情况会新增子对象 StyleRule 即可
于是乎发现 RuleList 对象的 register 函数每调用一次会在 this.map 对象上挂载一个 StyleRule 对象, 这妥妥的是缓存泄漏啊 ?
this.map
当我本地运行该项目也是印证了 Object.keys(this.map).length 一直在增长
Object.keys(this.map).length
你和我说一个较为流行的仓库 cssinjs/jss 会内存泄漏我是不太会相信, 至少可能性很小, 大概率还是业务项目的使用姿势有问题
让我们看看官方给的 Server-Side Rendering 使用的 demo, demo 代码很容易猜想到该代码的目的, 即每一次调用 render 函数需要先 new SheetsRegistry(), 然后通过 JssProvider 传递给子孙组件进行依赖收集。renderToString 函数运行结束即收集到了运行到的组件需要的样式, 最后通过 sheets.toString() 给吐出来
new SheetsRegistry()
JssProvider
sheets.toString()
这个行为和 react-loadable 收集动态模块一毛一样 ~ import React from 'react' import {renderToString} from 'react-dom/server' import {JssProvider, SheetsRegistry} from 'react-jss' import Button from './Button'
这个行为和 react-loadable 收集动态模块一毛一样 ~
import React from 'react' import {renderToString} from 'react-dom/server' import {JssProvider, SheetsRegistry} from 'react-jss' import Button from './Button'
export default function render() { const sheets = new SheetsRegistry()
const app = renderToString(
)
return '' + `<!DOCTYPE html>
背景
M 同学反映自己负责的 Next.js 项目疑似内存泄漏, 临近 618 需要尽快解决! 通过查看 Easy-Monitor 上的「堆内存趋势」曲线📈在一直上涨且不会下降就基本确定了是内存泄漏
问题排查
M 同学也迅速定位到了造成内存泄漏的 commit, 我仔细 review 了一下并没有发现全局变量缓存、闭包引用等高危操作
Easy-Monitor 下载快照
接着就只能从 Easy-Monitor 上间隔一段时间前后下载了两个堆快照,最后通过 Chrome Devtool Memory 面板的 Comparison 功能进行对比, 发现
StyleRule
对象净新增了 57042 个 ⚠️ !使用 devtoolx 分析
Chrome Devtool Memory 面板冗余信息多、可读性也较差, 推荐大家使用开源的 devtoolx 进行下一步的分析
尴尬的是开始跑 devtoolx 命令时遇见了下面的报错 好吧, 我还是使用 lldb 先定位 devtoolx 启动失败的问题, 结果发现通过 lldb 启动 devtoolx 又能够正常跑起来
此时排除了 devtoolx 不能识别该 v8 版本的 .heapsnapshot 文件以及系统调用 api 兼容性问题(松了口气, 还是能用 devtoolx ~)
看了一下 devtoolx 的代码, 猜想可能是打开 .heapsnapshot 文件失败了, 于是增加了如下代码再编译运行日志显示
ParseError: Operation not permitted
所以把 .heapsnapshot 文件从 Downloads 目录移了出来就愉快的跑了起来, 上面的代码也提交了一个 devtoolx/pull/18/, 最后作者发布了
devtoolx@1.0.2
版本 ❤️回归正题, 通过 devtoolx 分别对两个快照分析发现了
Object(674385)
的内存由 1.34MB 涨到了 34.95 MB ⚠️, 一展开发现是StyleRule
的父对象StyleRule
对象的引用关系是StyleSheet.RuleList.xxx.StyleRule
此时我们可以看看 Chrome Devtool Memory 面板的 Summary 功能查看是否有更多
Object(674385)
对象的信息, 最终确认了StyleRule
对象的引用关系是StyleSheet.RuleList.map.StyleRule
接着使用 Chrome Devtool Memory 面板的 Comparison 功能查看发现StyleRule
的父对象StyleSheet
与RuleList
并没有新增定位泄漏点
根据引用关系定位到了 npm 包 jss 的代码, 我们缩小范围直击
RuleList
对象在何种情况会新增子对象StyleRule
即可于是乎发现
RuleList
对象的 register 函数每调用一次会在this.map
对象上挂载一个StyleRule
对象, 这妥妥的是缓存泄漏啊 ?当我本地运行该项目也是印证了
Object.keys(this.map).length
一直在增长问题分析
你和我说一个较为流行的仓库 cssinjs/jss 会内存泄漏我是不太会相信, 至少可能性很小, 大概率还是业务项目的使用姿势有问题
让我们看看官方给的 Server-Side Rendering 使用的 demo, demo 代码很容易猜想到该代码的目的, 即每一次调用 render 函数需要先
new SheetsRegistry()
, 然后通过JssProvider
传递给子孙组件进行依赖收集。renderToString 函数运行结束即收集到了运行到的组件需要的样式, 最后通过sheets.toString()
给吐出来export default function render() { const sheets = new SheetsRegistry()
const app = renderToString(
)
return '' + `<!DOCTYPE html>