最近,Meta 開源了一款檢測 员咽,我們來一起看看這個框架有啥神奇之處吧~
2020 年赫段,Meta 的工程師將 Facebook.com 重構(gòu)為了單頁應(yīng)用(SPA),程序的大部分渲染和導(dǎo)航都會在客戶端使用 JavaScript 完成肪虎。后來他們又使用類似的架構(gòu)來重構(gòu)了 Meta 的大多數(shù)其他流行的網(wǎng)絡(luò)應(yīng)用程序,包括 Instagram 和 Workplace惧蛹。雖然這種架構(gòu)能夠提供更快的用戶交互扇救、更好的開發(fā)者體驗和更像原生應(yīng)用程序的感覺,但是在客戶端維護(hù) Web 應(yīng)用的狀態(tài)會讓內(nèi)存的管理變得更加復(fù)雜香嗓。
使用 Meta 網(wǎng)站的用戶經(jīng)常會快速注意到一些性能和功能正常使用的問題迅腔。然而,內(nèi)存泄漏就是另一回事了靠娱。它不會立即被察覺出來钾挟,因為它一次會占用一大塊內(nèi)存 — 然后逐漸影響整個 Web 會話并讓后續(xù)的交互和響應(yīng)變得更慢。
Meta 的工程師花費了大量時間來測試饱岸、優(yōu)化和控制頁面加載和交互時間,以及 JavaScript 的代碼大小徽千。相比之下苫费,他們在管理 Web 瀏覽器內(nèi)存方面做的工作并不多。當(dāng)分析新 Facebook.com 的內(nèi)存使用情況時双抽,發(fā)現(xiàn)客戶端的內(nèi)存使用情況和內(nèi)存不足 (OOM) 崩潰的數(shù)量一直在攀升百框。較高的內(nèi)存使用對頁面加載、交互性能牍汹、用戶參與度等核心指標(biāo)都有負(fù)面影響铐维。
為了幫助開發(fā)者解決這個問題柬泽,Meta 的工程師構(gòu)建了 MemLab,這是一個 JavaScript 內(nèi)存測試框架嫁蛇,可以自動進(jìn)行內(nèi)存泄漏檢測锨并,并且更容易找到內(nèi)存泄漏的根本原因。Meta 使用 MemLab 成功地控制了不可持續(xù)的內(nèi)存增長睬棚,并識別出了產(chǎn)品和基礎(chǔ)設(shè)施中的內(nèi)存泄漏和內(nèi)存優(yōu)化的一些手段第煮。
導(dǎo)致 Web 應(yīng)用內(nèi)存過高的原因
因為內(nèi)存泄漏通常不是很明顯,在開發(fā)過程中抑党,以及做 Code Review 的時候都很難發(fā)現(xiàn)包警,而且在生產(chǎn)環(huán)境中通常也很難找到根本原因。雖然主流的 JavaScript 運行時都有垃圾回收機(jī)制底靠,那么為什么還會有內(nèi)存泄漏呢害晦?
JavaScript 代碼中可能會有很多隱藏對象的引用,而隱藏的引用會以許多意想不到的方式導(dǎo)致內(nèi)存泄漏暑中。
例如:
var obj = {};
console.log(obj);
obj = null;
在 Chrome 中壹瘟,即使我們將引用設(shè)置為 null ,這段代碼也會泄漏 obj 痒芝。發(fā)生這種情況是因為 Chrome 需要保留對打印對象的內(nèi)部引用俐筋,以便以后可以在 Web 控制臺中對其進(jìn)行檢查(即使在 Web 控制臺沒打開的情況下)。
在某些情況下严衬,內(nèi)存在技術(shù)上并沒有發(fā)生泄漏澄者,而是在用戶會話期間線性增長而且沒有限制。最常見的原因是客戶端緩存沒有內(nèi)置任何釋放的邏輯请琳,無限滾動列表沒有任何虛擬化的功能粱挡,無法在添加新內(nèi)容時從列表中刪除較早的內(nèi)容。
我們也沒有適當(dāng)?shù)淖詣踊到y(tǒng)和流程來控制內(nèi)存俄精,因此防止此類問題的唯一防御措施就是專家通過 Chrome DevTools 定期挖掘內(nèi)存泄漏询筏,一些大型的項目幾乎每天都會有發(fā)布和變更,這樣的工作方式是不可持續(xù)的竖慧。
MemLab 的工作原理
MemLab 通過預(yù)定義的測試場景運行無頭瀏覽器并比較和分析 JavaScript 堆快照來發(fā)現(xiàn)內(nèi)存泄漏的問題嫌套。
這個過程可以分為下面六個步驟:
1.「瀏覽器交互」:MemLab 使用 Puppeteer 自動化瀏覽器,在目標(biāo)頁面上查找泄露的對象圾旨;
2.「區(qū)分堆」:導(dǎo)航到一個頁面然后離開它踱讨,正常情況下該頁面分配的大部分內(nèi)存也應(yīng)該被釋放,如果沒有砍的,可能暗示著存在內(nèi)存泄漏痹筛。MemLab 通過區(qū)分 JavaScript 堆并記錄在頁面 B 上分配的一組對象,這些對象沒有在頁面 A 上分配,但在重新加載頁面 A 時仍然存在帚稠,從而發(fā)現(xiàn)潛在的內(nèi)存泄漏谣旁;
3.「細(xì)化內(nèi)存泄漏列表」:內(nèi)存泄漏檢測器進(jìn)一步結(jié)合了特定框架的知識來細(xì)化泄漏對象的列表。例如滋早,React 分配的 Fiber 節(jié)點(React 用于渲染虛擬 DOM 的內(nèi)部數(shù)據(jù)結(jié)構(gòu))應(yīng)該在我們訪問多個選項卡后清理時釋放榄审。
4.「生成 retainer traces」:遍歷堆并為每個泄漏的對象生成 retainer traces 。trace 顯示了泄漏對象為何以及如何在內(nèi)存中保持活動狀態(tài)馆衔。打破引用鏈意味著泄漏的對象將不再可以從 GC 的根訪問瘟判,因此可以進(jìn)行垃圾回收。通過一步步地跟蹤角溃,就可以找到應(yīng)該設(shè)置為 null 的引用拷获;
5.「聚合 retainer traces」:將所有 retainer traces 聚集在一起,并為每個共享相似 retainer traces 的泄漏對象聚合顯示為一個跟蹤减细,其中還包括調(diào)試信息匆瓜,例如支配節(jié)點和保留大小。
6.「報告泄漏」:定期運行 MemLab未蝌,以持續(xù)收集 retainer traces驮吱,任何新的 traces 都會記錄到內(nèi)部儀表板,開發(fā)者可以查看每個內(nèi)存泄漏的 retainer traces 上的對象屬性萧吠。
MemLab 有哪些能力
「內(nèi)存泄漏檢測」
對于瀏覽器內(nèi)存泄漏的檢測左冬,MemLab 需要開發(fā)者提供的唯一輸入就是一個測試場景文件,這個文件定義了如何通過使用 Puppeteer API 和 CSS 選擇器覆蓋三個回調(diào)來與網(wǎng)頁交互纸型。MemLab 會自動區(qū)分 JavaScript 堆拇砰、優(yōu)化內(nèi)存泄漏并聚合結(jié)果。
「JavaScript 堆的 Graph-view API」
MemLab 支持一個自定義的泄漏檢測器狰腌,作為篩選器回調(diào)除破,應(yīng)用于每個由目標(biāo)交互分配的泄漏候選對象,但之后從不釋放琼腔。泄漏過濾器回調(diào)函數(shù)可以遍歷堆并確定哪些對象是內(nèi)存泄漏瑰枫。例如,我們的內(nèi)置檢漏器會跟蹤 React Fiber 節(jié)點的返回鏈路丹莲,檢查 Fiber 節(jié)點是否與 React Fiber 樹分離光坝。
為了分析每個可能內(nèi)存泄漏的上下文,MemLab 提供了一個 JavaScript 堆的內(nèi)存效率圖甥材。這可以在不了解 V8 堆快照文件結(jié)構(gòu)的任何領(lǐng)域知識的情況下查詢和遍歷 JavaScript 堆盯另。
在視圖中,堆中的每個 JavaScript 對象或原生對象都是一個圖節(jié)點擂达,堆中的每個 JavaScript 引用都是一個圖的邊。實際應(yīng)用程序的堆大小通常很大,因此圖視圖需要在提供直觀的面向?qū)ο蠖驯闅v API 的同時提高內(nèi)存效率板鬓。因此悲敷,圖節(jié)點被設(shè)計成了虛擬的,不通過 JavaScript 引用進(jìn)行連接俭令。當(dāng)分析代碼遍歷堆時后德,虛擬圖會部分地即時構(gòu)建圖的接觸部分。圖的任何部分都可以很容易地釋放抄腔,因為這些虛擬節(jié)點彼此之間沒有 JavaScript 引用瓢湃。
堆視圖可以從基于 Chromium 的瀏覽器、Node.js赫蛇、Electron 和 Hermes 獲取的 JavaScript 堆快照加載绵患。這允許分析復(fù)雜的模式并回答諸如 “有多少 React Fiber 節(jié)點是備用的 Fiber 節(jié)點,它們用于不完整的并發(fā)渲染悟耘?”之類的問題落蝙。
import {getHeapFromFile} from '@memlab/heap-analysis';
const heapGraph = await getHeapFromFile(heapFile);
heapGraph.nodes.forEach(node => {
// heap node traversal
node.type
node.references
);
「內(nèi)存斷言」
Node.js 程序或 Jest 測試也可以使用 graph-view API 來獲取其自身狀態(tài)的堆視圖,進(jìn)行自內(nèi)存檢查暂幼,并編寫各種內(nèi)存斷言筏勒。
import type {IHeapSnapshot} from '@memlab/core';
import {config, takeNodeMinimalHeap, tagObject} from '@memlab/core';
test('memory test', async () => {
config.muteConsole = true;
const o1 = {};
let o2 = {};
// tag o1 with marker: "memlab-mark-1", does not modify o1 in any way
tagObject(o1, 'memlab-mark-1');
// tag o2 with marker: "memlab-mark-2", does not modify o2 in any way
tagObject(o2, 'memlab-mark-2');
o2 = null;
const heap: IHeapSnapshot = await takeNodeMinimalHeap();
// expect object with marker "memlab-mark-1" exists
expect(heap.hasObjectWithTag('memlab-mark-1')).toBe(true);
// expect object with marker "memlab-mark-2" can be GCed
expect(heap.hasObjectWithTag('memlab-mark-2')).toBe(false);
}, 30000);
「內(nèi)存工具箱」
除了內(nèi)存泄漏檢測,MemLab 還包括一組內(nèi)置的 CLI 命令和 API旺嬉,用于尋找可能的內(nèi)存優(yōu)化機(jī)會:
Meta 使用 MemLab 的實踐
在過去的幾年中管行,Meta 一直在使用 MemLab 檢測和診斷內(nèi)存泄漏,并收集了很多有助于優(yōu)化內(nèi)存邪媳、減少 OOM 崩潰并改善用戶體驗的手段捐顷。
在 2021 年上半年, Facebook.com 上的 OOM 崩潰減少了 50%悲酷。
「React Fiber 節(jié)點清理」
為了渲染組件套菜,React 構(gòu)建了 Fiber 樹 — 一個 React 用于渲染虛擬 DOM 的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。雖然 Fiber 樹看起來像一棵樹设易,但它是一個雙向圖逗柴,將所有 Fiber 節(jié)點、React 組件實例和關(guān)聯(lián)的 HTML DOM 元素強(qiáng)連接起來顿肺。理想情況下戏溺,React 維護(hù)對組件 Fiber 樹的根的引用,并防止 Fiber 樹被垃圾回收屠尊。當(dāng)一個組件被卸載時旷祸,React 會斷開組件的根與 Fiber 樹的其余部分之間的連接,然后這些部分就可以被垃圾回收了讼昆。
擁有這樣的強(qiáng)連接圖的缺點是托享,如果有任何外部引用指向圖的任何部分,就無法對整個圖進(jìn)行垃圾回收。例如闰围,下面 export 語句在模塊范圍級別緩存 React 組件赃绊,因此相關(guān)的 Fiber 樹和分離的 DOM 元素永遠(yuǎn)不會被釋放。
export const Component = ((
<List> ... </List>
): React.Element<typeof List>);
也不僅僅是 React 數(shù)據(jù)結(jié)構(gòu)要 keep alive 羡榴,Hooks 和它們的閉包也可以讓各種其他對象北滩椋活。這意味著單個 React 組件泄漏可能會導(dǎo)致頁面對象的重要部分泄漏校仑,從而導(dǎo)致巨大的內(nèi)存泄漏忠售。
為了防止 Fiber 樹中內(nèi)存泄漏的級聯(lián)效應(yīng),MemLab 添加了一個樹的完整遍歷迄沫,當(dāng)組件在 React 18 中卸載時會進(jìn)行清理稻扬。這可以讓垃圾回收器在清理未掛載的樹方面做得更好一點。這個優(yōu)化將 Facebook 上的平均內(nèi)存使用量減少了近 25%邢滑,其他使用 React 的站點在升級時也有了很大的改進(jìn)腐螟。你可能會擔(dān)心這種比較激進(jìn)的清理方式可能會減慢 React 組件的卸載速度,但令人驚訝的是困后,由于內(nèi)存的減少乐纸,性能也有顯著的提升。
「string interning」
通過利用 MemLab 中的 heap analysis API摇予,Meta 團(tuán)隊發(fā)現(xiàn)字符串占據(jù)了 70% 的堆內(nèi)存汽绢,其中一半的字符串至少有一個重復(fù)的實例。(V8 對 string interning 支持的不是很好侧戴,這是一種對具有相同值的字符串實例進(jìn)行重復(fù)數(shù)據(jù)刪除的優(yōu)化宁昭。)
另外很大一部分字符串內(nèi)存被 Relay 中緩存的鍵字符串消耗。通過與 Relay 和 React Apps 團(tuán)隊合作酗宋,可以在客戶端插入和縮短過長的字符串鍵來優(yōu)化 Relay 緩存鍵字符串积仗。
這種優(yōu)化使 Relay 能夠緩存更多數(shù)據(jù),允許站點向用戶顯示更多內(nèi)容蜕猫,尤其是在客戶端 RAM 有限的情況下寂曹。內(nèi)存 p99 和 OOM 崩潰減少了 20%,頁面渲染速度更快回右,用戶體驗得到改善隆圆,在收入上也有一定提升。
試用 MemLab:
npm i -g memlab
最后
MemLab Github:https://github.com/facebookincubator/memlab