The Secret to Building Performance Libraries
Tags: Frontend Development, Nodejs, JavaScript, Web Development, React

The Secret to Building Performance Libraries

Two key reasons you need to understand delegate prototypes in JavaScript

Photo by Fabian Irsara @ Unsplash

I was reading a section in a book about JavaScript and I came across an issue (but also the power of the concept that the issue stems from) that I want to write about. I think it will be especially helpful for newcomers to JavaScript — even if you’re experienced you might learn something new!

This article will go over a known anti-pattern with delegate prototypes. If you’re a React user the concept of this anti-pattern may be familiar to you. We will also look at how you can use that concept to greatly improve the performance of your apps — just as the majority of JavaScript libraries today do.

So, if you want to create a library in JavaScript, I highly recommend you learn how you can optimize your app by delegating prototypes. This is called the Flyweight Pattern and will be explained in this piece.

If you don’t know what a prototype is, they’re objects that JavaScript uses to model other objects after. You could say they’re similar to classes in that they can construct multiple instances of objects, but they’re also objects themselves.

In JavaScript, all objects have some internal reference to a delegate prototype. When objects are queried by property or method lookups, JavaScript first checks the current object. If that doesn’t exist it then proceeds to check the object’s prototype, which is the delegate prototype, and then proceeds with that prototype’s prototype, and so on. When it reaches the end of the prototype chain the last stop ends at the root Object prototype. Creating objects attaches that root Object prototype at the root level. You can branch off objects with different immediate prototypes set with Object.create().

Let’s take a look at the code snippet below:

We have two factory functions here. One of them is makeSorceress which takes a type of sorceress as an argument and returns an object of the sorceress's abilities. The other is makeWarrior which takes a type of warrior as an argument and returns an object of the warrior's abilities.

We instantiate a new instance of the warrior class with type knight along with a sorceress with type fire .

We then used Object.create to create new objects for bob, joe, and lucy, additionally delegating the prototype objects for each.

Bob, joe, and lucy were set with their names on the instance, so that we claim and expect their own properties. Finally, bob attacks lucy with using bash , decreasing her HP by 10 points.

At a first glance, there doesn’t seem to be anything wrong with this example. But there is actually a problem. We expect bob and joe to have their own copy of properties and methods, which is why we used Object.create . When bob bashes lucy and inserts the last targeted name into the this.lastTargets.names array, the array will include the new target's name.

We can log that out and see it for ourselves:

The behavior is expected, however when we also log the last targeted names for joe , we see this:

That doesn’t make sense, does it? The person attacking lucy was bob, as is clearly seen above. But why was joe involved in the act? The one line of code explicitly writes bob.bash(lucy) , and that's it.

So the problem is that bob and joe are actually sharing the same state!

But wait, that doesn’t make any sense because we should have created their own separate copies when we used Object.create …or so we assumed.

Even the docs at MDN explicitly says that the Object.create() method creates a new object. It does create a new object — which it did — but the problem here is that if you mutate object or array properties on prototype properties, the mutation will leak and affect other instances that have some link to that prototype on the prototype chain. If you instead replace the entire property on the prototype, the change only occurs on the instance.

For example:

If you change the this.lastTargets.names property, it will be reflected with other objects that are linked to the prototype. However, when you change the prototype's property ( this.lastTargets ), it will override that property only for that instance. To a new developer this can be a little difficult to grasp.

Some of us who regularly develop apps using React have commonly dealt with this issue when managing state throughout our apps. What we probably never paid attention to is how that concept stems through the JavaScript language itself. So, to state this more clearly, it’s a problem with the JavaScript language in itself that this an anti pattern.

But why is this even an anti pattern? Can’t it be a good thing?

In certain ways it can be a good thing because you can optimize your apps by delegating methods to preserve memory resources. After all, every object just needs one copy of a method, and methods can be shared throughout all the instances, unless that instance needs to override it for additional functionality.

For example, let’s look back at the makeWarrior function:

The battleCry function is probably safe to be shared by all prototypes since it doesn't depend on any conditions to function correctly, besides an hp property which is already set upon instantiation. Newly created instances of this function do not necessarily need their own copy of battleCry and can instead delegate to the prototype object that originally defined this method.

Sharing data between instances of the same prototype is an anti-pattern because it can become very easy to accidentally mutate shared properties or data that shouldn’t be mutated. This has long been a common source of bugs for JavaScript applications.

This practice is in use for a good reason, in fact. Take a look at how the popular request package instantiates the Har function in this source code:

So why doesn’t Har.prototype.reducer just get defined like this?

As explained previously, if newer instances were to be instantiated, it would actually degrade the performance of your apps since it would be (recreating new methods on each instantiation), which is the reducer function.

When we have separate instances of Har :

We’re actually creating 5 separate copies of this.reducer in memory because the method is defined in the instance level. If the reducer was defined directly on the prototype, multiple instances of Har will delegate the reducer function to the method defined on the prototype!

This is an example of how to take advantage of delegate prototypes and improve the performance of your apps.