Heim  >  Artikel  >  Web-Frontend  >  Häufige JavaScript-Speicherlecks

Häufige JavaScript-Speicherlecks

小云云
小云云Original
2017-12-05 16:31:451721Durchsuche

Was ist ein Speicherverlust?

Ein Speicherverlust bezieht sich auf ein Programm, das aufgrund von Fahrlässigkeit nicht veröffentlicht werden kann oder Fehler und wird nicht mehr verwendet. Speicherlecks beziehen sich nicht auf das physische Verschwinden von Speicher, sondern auf die Tatsache, dass die Anwendung, nachdem sie aufgrund eines Entwurfsfehlers ein bestimmtes Speichersegment zugewiesen hat, die Kontrolle über das Speichersegment verliert, bevor es freigegeben wird, was zu einem Verschwendung von Speicher.
Speicherlecks können normalerweise nur von Programmierern analysiert werden, die Zugriff auf den Quellcode des Programms haben. Viele Menschen sind es jedoch gewohnt, jede unerwünschte Erhöhung der Speichernutzung als Speicherverlust zu bezeichnen, auch wenn dies nicht ganz korrekt ist.
————Wikipedia

Unerwartete globale Variable

Wie JavaScript mit nicht deklarierten Variablen umgeht: Erstellen Sie im globalen Kontext eine Referenz auf die Variable des Objekts (d. h. eine Eigenschaft des globalen Objekts, keine Variable, da sie durch Löschen gelöscht werden kann). Wenn Sie sich in einem Browser befinden, ist das globale Objekt das Fensterobjekt.

Wenn nicht deklarierte Variablen eine große Datenmenge zwischenspeichern, können die Daten nur freigegeben werden, wenn das Fenster geschlossen oder die Seite aktualisiert wird. Dies kann zu unerwarteten Speicherlecks führen.

<span style="font-size: 14px;">function foo(arg) {<br>    bar = "this is a hidden global variable with a large of data";<br>}<br></span>

ist äquivalent zu:

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is an explicit global variable with a large of data";<br>}<br></span>

Darüber hinaus werden unerwartete globale Variablen erstellt:

<span style="font-size: 14px;">function foo() {<br>    this.variable = "potential accidental global";<br>}<br><br>// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'<br>foo();<br></span>

Lösung:

Fügen Sie „use strict“ in der JavaScript-Datei hinzu und aktivieren Sie den strikten Modus, wodurch die oben genannten Probleme effektiv vermieden werden können.

<span style="font-size: 14px;">function foo(arg) {<br>    "use strict" // 在foo函数作用域内开启严格模式<br>    bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明<br>}<br></span>

Wenn Sie globale Variablen in einer Funktion verwenden müssen, können Sie diese explizit im Fenster deklarieren, wie im folgenden Code gezeigt:

<span style="font-size: 14px;">function foo(arg) {<br>    window.bar = "this is a explicit global variable with a large of data";<br>}<br></span>

Dies ist nicht nur gut lesbar, sondern auch praktisch für die spätere Wartung

Wenn es um globale Variablen geht, müssen Sie auf diese globalen Variablen achten Wird zum vorübergehenden Speichern großer Datenmengen verwendet und stellt sicher, dass sie nach der Verarbeitung dieser Daten auf Null gesetzt oder neu zugewiesen werden. Globale Variablen werden im Allgemeinen auch zur Leistungsoptimierung verwendet. Für die Leistung ist es am besten, eine Obergrenze für die Cachegröße festzulegen. Da der Cache nicht wiederhergestellt werden kann, führt ein höherer Cache zu einem höheren Speicherverbrauch.

console.log

console.log: Gibt eine Nachricht an die Webentwicklungskonsole aus, die häufig zum Debuggen und zur Analyse verwendet wird Entwicklung . Manchmal müssen Sie während der Entwicklung einige Objektinformationen drucken, vergessen jedoch beim Veröffentlichen, die Anweisung console.log zu entfernen, was zu Speicherverlusten führen kann.

Das an console.log übergebene Objekt kann nicht durch Müll gesammelt werden ♻️, da das Entwicklungstool in der Lage sein muss, die Objektinformationen anzuzeigen, nachdem der Code ausgeführt wurde. Daher ist es am besten, keine Objekte in der Produktionsumgebung console.log.

Beispiel------>demos/log.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Leaker</title><br></head><br><br><body><br>  <input type="button" value="click"><br>  <script><br>    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>          console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br>  </script><br></body><br><br></html><br></span>

Hier kombiniert mit den Devtools von Chrome–>Leistung Führen Sie eine Analyse durch. Die Schritte lauten wie folgt:

⚠️Hinweis: Führen Sie die Analyse am besten in einem ausgeblendeten Fenster durch, um zu vermeiden, dass Browser-Plugins die Analyseergebnisse beeinflussen

  1. Öffnen Sie den Datensatz des Elements [Leistung]

  2. Führen Sie CG einmal aus und erstellen Sie eine Basisreferenzlinie

  3. Klicken Sie dreimal hintereinander auf die Schaltfläche [Klicken], um drei neue Leaker-Objekte zu erstellen

  4. CG einmal ausführen

  5. Aufnahme stoppen

Häufige JavaScript-Speicherlecks

Es ist ersichtlich, dass sich die [JS Heap]-Zeile am Ende befindet. Ohne auf die Basisreferenzlinie zurückzugreifen, gibt es offensichtlich Speicher, der nicht zurückgefordert wurde. Wenn Sie den Code wie folgt ändern:

<span style="font-size: 14px;">    !function () {<br>      function Leaker() {<br>        this.init();<br>      };<br>      Leaker.prototype = {<br>        init: function () {<br>          this.name = (Array(100000)).join('*');<br>        },<br><br>        destroy: function () {<br>          // do something....<br>        }<br>      };<br>      document.querySelector('input').addEventListener('click', function () {<br>        new Leaker();<br>      }, false);<br>    }()<br></span>

entfernen Sie die Anweisung console.log("Leaking an object %o: %o", (new Date()), this); . Wiederholen Sie die obigen Schritte und die Analyseergebnisse lauten wie folgt:

Häufige JavaScript-Speicherlecks

Aus den Vergleichsanalyseergebnissen ist ersichtlich, dass die Objekte von der Konsole gedruckt wurden .log wird nicht recycelt. Daher ist es am besten, keine großen Objekte auf der Seite zu console.loggen, da dies die Gesamtleistung der Seite beeinträchtigen kann, insbesondere in einer Produktionsumgebung. Neben console.log gibt es auch ähnliche Probleme wie console.dir, console.error, console.warn usw. Diese Details erfordern besondere Aufmerksamkeit.

closures(闭包)

当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。

<span style="font-size: 14px;">function foo(message) {<br>    function closure() {<br>        console.log(message)<br>    };<br>    return closure;<br>}<br><br>// 使用<br>var bar = foo("hello closure!");<br>bar()// 返回 'hello closure!'<br></span>

在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

实例------>demos/closures.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Closure</title><br></head><br><br><body><br>  <p>不断单击【click】按钮</p><br>  <button id="click_button">Click</button><br>  <script><br>    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        str = 'unused: ' + str;<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br>  </script><br></body><br><br></html><br></span>

这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:

⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Record allocation timeline】选项

  2. 执行一次CG

  3. 单击【start】按钮开始记录堆分析

  4. 连续单击【click】按钮十多次

  5. 停止记录堆分析

Häufige JavaScript-Speicherlecks

上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:

Häufige JavaScript-Speicherlecks

查看对象的详细信息:

Häufige JavaScript-Speicherlecks

从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉, why

原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:

<span style="font-size: 14px;">    function f() {<br>      var str = Array(10000).join('#');<br>      var foo = {<br>        name: 'foo'<br>      }<br>      function unused() {<br>        var message = 'it is only a test message';<br>        // str = 'unused: ' + str; //删除该条语句<br>      }<br>      function getData() {<br>        return 'data';<br>      }<br>      return getData;<br>    }<br><br>    var list = [];<br>    <br>    document.querySelector('#click_button').addEventListener('click', function () {<br>      list.push(f());<br>    }, false);<br></span>

getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;语句删除便可解决问题。

Häufige JavaScript-Speicherlecks

查看一下闭包信息:

Häufige JavaScript-Speicherlecks

DOM泄露

在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️

Häufige JavaScript-Speicherlecks

为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。

实例------>demos/dom.html

<span style="font-size: 14px;"><!DOCTYPE html><br><html lang="en"><br><head><br>  <meta charset="UTF-8"><br>  <meta name="viewport" content="width=device-width, initial-scale=1.0"><br>  <meta http-equiv="X-UA-Compatible" content="ie=edge"><br>  <title>Dom-Leakage</title><br></head><br><body><br>  <input type="button" value="remove" class="remove"><br>  <input type="button" value="add" class="add"><br><br>  <p class="container"><br>    <pre class="wrapper">