从服务器端渲染的网站切换到客户端渲染的 SPA 时,我们突然不得不更加注意用户设备上的资源,必须做很多工作:不要阻塞 UI 线程,不要使笔记本电脑的风扇疯狂旋转,不要耗尽手机的电池等。我们将交互性和“类应用程序”行为转换成了更好的新型问题,这些问题实际上并不存在在服务端渲染的世界中。
这些问题中最主要的一个是内存泄漏。编码不正确的 SPA 可能很容易耗尽 MB 甚至 GB 的内存,从而继续吞噬越来越多的资源,即使它无辜地存在于后台标签中也是如此。这时页面可能开始变成龟速,或者浏览器终止了标签页,你将会看到熟悉的 “Aw, snap!” 页面。
(当然,服务端渲染的网站也可能会泄漏服务器端的内存。但是客户端泄漏内存的可能性很小,因为每次你在页面之间导航时浏览器都会清除内存。)
Web 开发文献中没有很好地解决内存泄漏问题的方法。但是,我非常确定大多数不凡的 SPA 都会泄漏内存,除非它们背后的团队拥有强大的基础结构来捕获和修复内存泄漏。用 JavaScript 太容易了,以至于不小心分配了一些内存而忘了清理它。
那么,为什么关于内存泄漏的文章这么少呢?我的猜测是:
在本文中,我想分享一些我在解决 Web 程序中的内存泄漏方面的经验,并提供一些示例来说明如何有效地跟踪它们。
像 react、vue 和 Svelte 这样的现代 Web 框架都使用基于组件的模型。在此模型中,产生内存泄漏的最常见方法是这样的:
window.addEventListener('message', this.onMessage.bind(this));
就这样,引入了一个内存泄漏。如果你在某些全局对象(window、<body> 等)上调用 addEventListener 然后在卸载组件时忘记用 removeEventListener 进行清理,就会产生一个内存泄漏。
更糟糕的是,你刚刚泄漏了整个组件。由于 this.onMessage 绑定到 this,所以组件已泄漏,包括其所有子组件。而且很可能所有与组件相关联的 dom 节点也是如此。这会很快会变得非常糟糕。
解决方法是:
// Mount phase
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
// Unmount phase
window.removeEventListener('message', this.onMessage);
注意,我们保存了对绑定的 onMessage 函数的引用。你必须把前面传给 addEventListener 的函数再原封不动的传给 removeEventListener,否则它将无法正常工作。
以我的经验,最常见的内存泄漏源与以下 api 相关:
当然,还有许多其他导致泄漏内存的情况,但这些是最常见的。
这是困难的部分。首先我要说的是,我认为那里的任何工具都不是很好。我尝试使用 Firefox 的内存工具,Edge 和 IE 内存工具,甚至 Windows Performance Analyzer。同类最佳的仍然是 Chrome Dev Tools,但是它有很多杂乱的细节值得我们了解。
在 Chrome Dev Tools中,我们选择的主要工具是“内存(Memory)”标签中的“堆快照(heap snapshot)”。 Chrome 中还有其他存储工具,但我发现它们对识别泄漏不是很有帮助。
带有堆快照工具的Chrome DevTools内存选项卡
堆快照工具使你可以捕获主线程、Web Worker 或 iframe 的内存。
当你点击“获取快照(take snapshot)”按钮时,你已经捕获了该网页上特定 JavaScript VM 中的所有活动对象。这包括 window 所引用的对象,setInterval 回调所引用的对象等。可将其视为时间暂停后,代表该网页使用的所有内存。
下一步是重现你认为可能正在泄漏的某些场景,例如,打开和关闭模态对话框。对话框关闭后,你希望内存恢复到上一级。因此,你获取了另一个快照,然后将其与上一个快照进行比较。这种差异确实是该工具的杀手级特性。
显示第一个堆快照的示意图,然后是一个泄漏的场景,然后是第二个堆快照,该快照应该等于第一个
但是,你应该注意该工具的一些限制:
此时,如果你的程序很复杂,那么可能会在两个快照之间看到大量的泄漏对象。这是棘手的地方,因为并非所有这些都是真正的泄漏。其中许多只是正常用法——某些对象被取消分配,而另一个对象被优先分配,某些对象以某种方式被缓存,以便稍后进行清理,等等。
我发现消除噪音的最好方法是多次重复泄漏情况。例如,你不仅可以执行一次打开和关闭模式对话框这种操作,还可以将其打开和关闭 7 次。 (7 是一个质数。)然后你可以检查堆快照 diff,以查看是否有什么对象泄漏7次。 (或14次或21次。)
Chrome开发者工具堆快照差异的截图显示了六个堆快照捕获,其中有多个对象泄漏了7次
堆快照差异。请注意,我们正在将 6 号快照与 3 号快照进行比较,因为我连续拍摄了三个快照,以便进行更多的垃圾收集。注意,有几个对象泄漏了 7 次。
(另一种有用的技术是在记录第一个快照之前对方案进行一次遍历。特别是如果你进行大量的代码拆分,则方案可能会花费一次内存来加载必要的 JavaScript 模块。)
你可能想知道为什么应该按对象数而不是总内存进行排序。直观地讲,我们正在努力减少内存泄漏的数量,所以我们不应该专注于总的内存使用情况吗?嗯,这不是很好,有一个很重要的原因。
当什么东西泄漏时,是因为你想要得到香蕉,但是最终得到的是香蕉、拿着香蕉的大猩猩以及整个丛林。如果你基于总字节数进行衡量,那么你所衡量的是丛林,而不是香蕉。
让我们回到上面的 addEventListener 的例子。泄漏的来源是事件侦听器,该事件侦听器引用一个函数,该函数引用一个组件,该组件可能引用大量的东西,例如数组、字符串和对象。
如果你按总内存对堆快照差异进行排序,那么它将向你显示一堆数组、字符串和对象——其中大多数可能与泄漏无关。你真正想要找到的是事件侦听器,但是与它所引用的内容相比,占用的内存很小。要修复泄漏,你要找到香蕉,而不是丛林。
所以,如果按泄漏对象的数量进行排序,则会看到 7 个事件监听器。可能是 7 个组件和 14 个子组件等等。 “7” 应该像腰间盘一样突出,因为它是一个不寻常的数字。而且,无论你重复该场景多少次,都应该确切的看到泄漏的对象数量。这样可以快速找到泄漏源。
堆快照差异还将向你显示一个 “retainer” 链,该链显示哪些对象指向哪些其他对象,从而使内存保持活动状态。这样可以弄清楚泄漏对象的分配位置。
事件监听器引用的闭包所引用的 someObject 的固定链
retainer 链将向你显示哪个对象正在引用泄漏的对象。读取它的方式是每个对象都由其下面的对象引用。
在上面的示例中,有一个名为 someObject 的变量,该变量由闭包(也称为“上下文”)引用,并由事件侦听器引用。如果单击源链接,它将带你到 JavaScript 声明,这很简单:
class SomeObject () { /* ... */ }
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);
在上面的示例中,“上下文”是 onMessage 闭包,它引用了 someObject 变量。 (这是一个人为的例子;实际的内存泄漏可能不那么明显!)
但是堆快照工具有几个限制:
这是识别内存泄漏的基本策略。我过去已经成功地用这种技术发现了许多内存泄漏。
但是,本指南只是一个开始——除此之外,你还必须随手设置断点、记录日志并测试你的修复程序,以查看它是否可以解决泄漏。不幸的是,这是一个非常耗时的过程。
在此之前,我要说的是,我还没有找到一种自动检测内存泄漏的好方法。 Chrome 有非标准的performance.memory API,但出于隐私方面的考虑它没有非常精确的粒度,因此你不能真正在生产中用它来识别泄漏。 W3C 网络性能工作组过去讨论了内存 工具,但尚未就取代该 API 的新标准达成共识。
在实验室或综合测试环境中,你可以用 Chrome 标志 --enable-precise-memory-info。还可以通过调用专有的 Chromedriver 命令 :takeHeapSnapshot 创建堆快照文件。但是这也具有上述相同的限制——你可能想要连续获取三个并丢弃前两个。
由于事件监听器是最常见的内存泄漏源,因此我使用的另一种技术是对 monkey-patch 的 addEventListener 和 removeEventListener API进行计数,从而进行计数引用并确保它们返回零。这里是如何执行此操作的示例。
在 Chrome Dev Tools 中,你还可以使用专有的 getEventListeners() API 来查看事件监听器附加到特定元素。注意,这只能在 Dev Tools 中使用。
在 Web 应用中查找和修复内存泄漏的状态仍然很初级。在本文中,我介绍了一些对我有用的技术,但是请记住,这仍然是一个困难且耗时的过程。
与大多数性能问题一样,少量预防胜过大量的治疗。你可能会发现进行综合测试是值得的,而不是在事实发生后尝试调试内存泄漏。尤其是如果页面上存在多个泄漏,则可能会变成洋葱剥皮练习——你先修复一个泄漏,然后查找另一个泄漏,然后重复(整个过程都在哭泣!)。如果你知道要查找的内容,代码审查还可以帮助捕获常见的内存泄漏模式。
JavaScript 是一种内存安全的语言,具有讽刺意味的是,在 Web 应用中泄漏内存有多么容易。不过部分原因只是 UI 设计所固有的——我们需要侦听鼠标事件、滚动事件、键盘事件等,而这些都是容易导致内存泄漏的模式。但是,通过尝试降低 Web 应用的内存使用量,可以提高运行时性能,避免崩溃,并尊重用户设备上的资源限制。
感谢 Jake Archibald 和 Yang Guo 对本文的草稿提供反馈。感谢 Dinko Bajric 发明了“choose a prime number”技术,我发现它在内存泄漏分析中非常有用。
作者:Nolan Lawson
翻译:疯狂的技术宅
原文:https://nolanlawson.com/2020/02/19/fixing-memory-leaks-in-web-applications/
JavaScript 的内存管理和垃圾回收,是个略生僻的话题,因为在JavaScript 中不显式执行内存操作,不过最好了解它如何工作。
js具有自动回收垃圾的机制,即执行环境会负责管理程序执行中使用的内存。在C和C++等其他语言中,开发者的需要手动跟踪管理内存的使用情况。在编写js代码时候,开发人员不用再关心内存使用的问题,所需内存的分配 以及无用的回收完全实现了自动管理。
javaScript内存空间并不是一个经常被提及的概念,想要对JS的理解更加深刻,就必须对内存空间有一个清晰的认:栈与堆、复杂数据类型与基本数据类型、引用数据类型与堆内存
实际上并不是新建一个和原对象(数组也是对象)完全一样的对象,而是把原对象的内存地址直接复制给了另一个对象,也就是说两个对象都是指向同一个内存地址,所以实际上它们就是同一个对象。
php垃圾回收机制,对于PHPer来说是一个不陌生但是又不是很熟悉的内容。那么php是怎么实现对不需要的内存进行回收的呢?首先还是需要了解下基础知识,便于垃圾回收原理内容的理解。
JavaScript变量可以用来保存两种类型的值:基本类性值和引用类性值。所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题
计算机的内存由操作系统进行管理,所以普通应用程序是无法直接对内存进行访问的。应用程序只能向操作系统申请内存,通常的应用也是这么做的,在需要的时候通过类似malloc之类的库函数 向操作系统申请内存。
当项目以tab页签方式打开多个iframe窗口时,关闭tab页签同时也需要关闭iframe并释放内存资源,主要是针对IE浏览器。
今天遇到一个很有争议的问题,在这里分享一下,我相信对于即将面试前端的小伙伴会有帮助的。主要内容是围绕下边的问题展开的,文章涉及到的其他方面的知识点不展开叙述。
随着 Web 应用复杂程度越来越高,以及 NodeJS 大规模投入生产环境,许多 Web 应用都会长时间运行, JavaScript 的内存管理显得更为重要。JavaScript 具备自动回收垃圾的机制
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!