33

I would like to determine which is the best practice between equivalent solutions. The use case is an instance of a class that listen to an event. Dr. Axel Rauschmayer prefers the lambda for readability. I agree with him. But in term of performance and memory consumption, which is the best?

With a lambda function

class Abc {
  constructor() {
    let el = document.getElementById("my-btn")
    if (el)
      el.addEventListener("click", evt => this.onClick(evt))
  }
  onClick(evt) {
    console.log("Clicked!", evt.target)
  }
}

Can someone to confirm or infirm if the local variables (here el) can't be cleared by the garbage collector? Or, are modern browsers capable to detect they are unused in the closure?

With Function.prototype.bind:

class Abc {
  constructor() {
    let el = document.getElementById("my-btn")
    if (el)
      el.addEventListener("click", this.onClick.bind(this))
  }
  onClick(evt) {
    console.log("Clicked!", evt.target)
  }
}

There is no memory issue, but all benchmarks suggest that bind is far slower than a closure (an example here).

EDIT: I don't agree with the comments that ignore the performance issue of bind. I suggest to read this answer with the code of the implementation in Chrome. It cannot be efficient. And I insist: all the benchmarks I saw, show similar results on all browsers.

Is there a way to have a low memory usage and a good performance at the same time?

Community
  • 1
  • 1
Paleo
  • 21,831
  • 4
  • 65
  • 76
  • 2
    Syntax doesn't have performance. Implementations do. So the answer could only be "it depends". With the closure example in the jsPerf being that much faster in your tests (in Firefox anyway), I'd be inclined to suggest that there's an optimization kicking in that's eliminating the call altogether, which may not be available in real world code. –  Feb 08 '17 at 16:05
  • I suggest event delegation to reduce the number of binds, with the logic to act on the event a method in the object. I understand this is not with the OP asked, but am mentioning it as a point worth considering. – user2182349 Feb 08 '17 at 16:06
  • 5
    And just FYI, given your example, you could actually do `el.addEventListener("click", this)`, and then give your class a `handleEvent` method. When the event occurs, `handleEvent` will be invoked. https://jsfiddle.net/8qkfxshj/ –  Feb 08 '17 at 16:09
  • Are these actually functionally equivalent? In the bound version, the instance of Abc can be reached inside the event handler through 'this', which you cannot in the lambda version. So i would use bind if I have need to reach the instance. If only the event.target (aka the element my-btn ) is needed, I'd usually prefer the lambda. – Shilly Feb 08 '17 at 16:10
  • @Shilly: His examples are equivalent. An arrow function doesn't have its own `this` value, so it uses the one it closes over. –  Feb 08 '17 at 16:11
  • Thanks, I had a hunch there was something I'm missing. So much ES6 to learn. – Shilly Feb 08 '17 at 16:12
  • @squint Thank you for the trick of `handleEvent`. Maybe it is the best answer: the API should provide a way to pass the context object `this`. But the question remains, for example with promises or other. – Paleo Feb 08 '17 at 16:15
  • You're welcome. Unfortunately, I honestly don't think there's a simple answer to that question. How and when an implementation can optimize a particular bit of code can vary depending on the actual situation. I've seen some perf tests that put `.bind()` functions in the lead (again, in Firefox). And that could change with the next release. You really need to figure out which parts of your app are most performance critical, and then work on fine tuning those specific areas by performing tests that use the actual real-world code. –  Feb 08 '17 at 16:20
  • @squint @nem035 I edited to add a link on the performance issue of `bind`. – Paleo Feb 08 '17 at 16:22
  • *"...with the code of the implementation in Chrome. It cannot be efficient."* How does a user posting a chunk of code from a specific release of a specific implementation translate to `.bind()` not being able to be efficient? –  Feb 08 '17 at 16:23
  • @squint Because of the algorithm described in the specification just above. – Paleo Feb 08 '17 at 16:25
  • @Paleo: That's a common mistake people make when reading a spec. The spec doesn't govern how the code must be written. It only describes the manner in which the code must behave. In other words, as long as the code *acts* as though it was written with that algorithm, then it has met the requirements. Who knows how many of those steps can be entirely skipped because an optimizing compiler determined that in certain situations they're impossible to reach. –  Feb 08 '17 at 16:27
  • [ECMAScript 6, Section 5.2](http://www.ecma-international.org/ecma-262/6.0/#sec-algorithm-conventions) *"The specification often uses a numbered list to specify steps in an algorithm. These algorithms are used to precisely specify the required semantics of ECMAScript language constructs. The algorithms are not intended to imply the use of any specific implementation technique. In practice, there may be more efficient algorithms available to implement a given feature."* –  Feb 08 '17 at 16:33
  • @squint OK OK you're right, the spec could be implemented in a better way. It's not the case today as we can test on jsperf. But the memory issue of the lambda function could be solved too. I really would like to know if current browsers have solved the memory issue with the lambda function or no. – Paleo Feb 08 '17 at 16:33
  • Additionally, Benjamin left [a comment](http://stackoverflow.com/questions/17638305/why-is-bind-slower-than-a-closure/17638540#comment25683443_17638540) under his answer to that effect as well. –  Feb 08 '17 at 16:36
  • As I noted above, it seems more likely that the closure example was entirely optimized away. No offense, but no conclusions should be made from such a simple test. Again, I've seen perf tests that put `.bind()` in the lead. And what memory issue have you actually experienced? –  Feb 08 '17 at 16:37
  • 2
    If it's just about confirmation that there's no issue, consider it generally confirmed. Modern GC implementations handle that in their sleep. –  Feb 08 '17 at 16:38
  • @squint Do you have a link to explain that (modern GC implementation)? I'm really interested. Please, write an answer with the suggestion to use the API when a way is provided to pass the `this` context, and don't forget to mention that modern GC implementation can detect and remove unused variables visible from the closure. I'll validate your answer. – Paleo Feb 08 '17 at 16:42
  • It seems the answer to my question is something like: All the options are OK for memory consumption (but the lambda needs more work from the GC). And all options should be OK for performance in the future (even if `bind` is slow with current implementations). – Paleo Feb 08 '17 at 16:48
  • I can't prove the absence of something. I can only tell you that people long struggled with memory leaks that came from circular references, including those created by closures, especially in older versions of IE. Those leaks were demonstrated by various people at various times to no longer exist. I did some testing myself where I generated thousands of elements, each of which made direct references to each other, as well as closure references, just to see if the GC would clean it all up. They handled it just fine, though it took some seconds to get there. This was a few years ago. –  Feb 08 '17 at 16:49
  • None of this means there's no leak hiding somewhere that hasn't yet been discovered. But don't worry about it until you find it. Future implementations can have regressions. Don't worry about that either. It would be impossible to avoid. –  Feb 08 '17 at 16:50

1 Answers1

31

Closures (or arrow functions, aka lambdas) don't cause memory leaks

Can someone to confirm or infirm if the local variables (here el) can't be cleared by the garbage collector? Or, are modern browsers capable to detect they are unused in the closure?

Yes, modern JavaScript engines are able to detect variables from parent scopes that are visible from a closure but unused. I found a way to prove that.

Step 1: the closure uses a variable of 10 MB

I used this code in Chromium:

class Abc {
    constructor() {
        let arr = new Uint8Array(1024*1024*10) // 10 MB
        let el = document.getElementById("my-btn")
        if (el)
            el.addEventListener("click", ev => this.onClick(ev, arr))
    }
    onClick(ev) {
        console.log("Clicked!", ev.target)
    }
}

new Abc()

Notice the variable arr of type Uint8Array. It is a typed array with a size of 10 megabytes. In this first version, the variable arr is used in the closure.

Then, in the developer tools of Chromium, tab "Profiles", I take a Heap Snapshot:

Snapshot 1: the variable <code>arr</code> is used in the closure

After ordering by decreasing size, the first row is: "system / JSArrayBufferData" with a size of 10 MB. It is our variable arr.

Step 2: the variable of 10 MB is visible but unused in the closure

Now I just remove the arr parameter in this line of code:

            el.addEventListener("click", ev => this.onClick(ev))

Then, a second snapshot:

Snapshot 2: the variable <code>arr</code> is not used in the closure

The first row has vanished.

This experience confirms that the garbage collector is capable to clean variables from parent scopes that are visible but unused in active closures.

About Function.prototype.bind

I quote the Google JavaScript Style Guide, section on arrow functions:

Never call f.bind(this) or goog.bind(f, this) (and avoid writing const self = this). All of these can be expressed more clearly and less error-prone with an arrow function. This is particularly useful for callbacks, which sometimes pass unexpected additional arguments.

Google clearly recommends to use lambdas rather than Function.prototype.bind.

Related:


Community
  • 1
  • 1
Paleo
  • 21,831
  • 4
  • 65
  • 76
  • 1
    Closures can cause memory leaks but only in more complex cases than shown above. https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156 The article describes how multiple lambda functions in the same context share their closures. If the 10mb variable is used by one lambda function, any other lambda function that could see it will keep it alive (even if it does not use the variable itself) – Ilmarinen123 Oct 25 '21 at 20:49