JS prototypes are slow ... ?
Tags: performance, javascript, node
Published: 2022-11-08 16:19:25

On a project I'm working on, I came across a strange requirement (which I'll explain in a bit) and most importantly, it needed to be performant. After some testing I concluded that going with the obvious native faster solution wasn't actually the faster solution...

To best explain the requirement, let's bring in the traditional example of cars
![car.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lyczl2oj4mc7nnh0ia10.jpg)
```js
class Car{
  constructor(topSpeed, color, name){
    this.position = 0
    this.topSpeed = topSpeed
    this.color = color
    this.name = name
  }
}
let firstCar = new Car(110, 'red', 'Honda Civic')
```

Unfortunately, as simple and elegant as this looks, it may not be the best idea. Can you see why? Well when I want to implement logic, the Honda's engine may work differently from a Ford, and so I would be forced to write a really large switch statement for each car type.

No problem! This kind of problem has been addressed before, just create a HondaCivic class that extends the Car class.
```js
class Car{
  constructor(topSpeed, color, name){
    this.position = 0
    this.topSpeed = topSpeed
    this.color = color
    this.name = name
  }
}
class HondaCivic extends Car{
  constructor(color){
    super(110, color, 'Honda Civic')
  }
  startUp(){
    //Logic to start our honda
  }
}
```
Another problem arises, if we wanted to track 1000+ honda civics, we would have 1000+ times the name `'Honda Civic'` in memory, as well as any other properties of the Honda civic family. If you've ever used function-style constructors before, you'd know there's a simple solution to that too:
```js
class Car{
  constructor(color){
    this.position = 0
    this.color = color
  }
}
class HondaCivic extends Car{
  startUp(){
    //...
  }
}
//Instead of defining name & topSpeed on instances
//we define it on the prototype
HondaCivic.prototype.name = 'Honda Civic'
HondaCivic.prototype.topSpeed = 110

let firstCar = new HondaCivic('red')
console.log(firstCar.topSpeed == 110) //true
```
# The big issue
Now let's say we want to test our car around a track
```js
function testCar(car){
  let seconds = 0
  //run car around a 100km track while counting seconds
  while(car.position < 100 * 1000){
    car.position += car.topSpeed
    seconds++
  }
  return seconds
}
```
Now, don't get me wrong, this function works fine and performs about as good as you can get, _or does it_?
Let's time how fast js can execute this function

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r13t8i4v0rzoh39ue6ek.png)

Neat! I'm sure no one would need to race their car 150 million times per second around a track
Let's try adding more cars
```js
class FordMondeo extends Car{...}
FordMondeo.prototype.topSpeed = 120
FordMondeo.prototype.name = 'Ford Mondeo'
class BmwM4 extends Car{...}
...
```
After we've added 4 cars and we've tested them all, we go back to our Honda civic and run the function again

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y3gzgiyuqql37kqk22sg.png)

_What??_

When we ran our test earlier, we got 156 million ops/s, now we only get 110. What happened?
Earlier, the `testCar` function was optimized to recieve exclusively `HondaCivic` objects. Now that we've tested a bunch of more cars, the Javascript engine can no longer be sure what car the function will recieve.
As such, it no longer knows for sure that the car we're testing has the same properties with the same type. _But hang on_, all the cars we tested _did_ have the same properties and even the same types.
Well, that's the magic of optimizations, we never know for sure what's going on under the hood or why it does something that way. There may be a reason, or it may just be a silly mistake from V8's developers. ¯\\_(ツ)_/¯

# The solution

Enough explaining and complaining, is there a way to get around this so fundamental issue?
As it turns out, yeah and it's really simple:

> Avoid needing multiple classes: don't use the prototype

What if, instead of creating a new class with different properties & methods, we put them in an object, a `CarDefinition` that we can attach to each car?
```js
class Car{
  constructor(definition, color){
    this.props = definition
    this.position = 0
    this.color = color
  }
}
const HondaCivic = {name: 'Honda Civic', topSpeed: 110}

let firstCar = new Car(HondaCivic, 'red')
console.log(firstCar.props.topSpeed == 110) //true
```
Now that we only use 1 class `Car` for all types of cars, `testCar` will stay optimized, as it should.
_But_, you may complain, _I don't want to have to access `.props` every time, I'm a programmer, and programmers are lazy_
Don't worry, I got you covered. Let's add some getters for a shortcut
```js
class Car{
  constructor(definition, color){
    this.props = definition
    this.position = 0
    this.color = color
  }
  get topSpeed(){return this.props.topSpeed}
  get name(){return this.props.name}
}
const HondaCivic = {name: 'Honda Civic', topSpeed: 110}

let firstCar = new Car(HondaCivic, 'red')
console.log(firstCar.topSpeed == 110) //true
```
Now, we can use our object just as before, without having to worry about `props`
# Performance of getters
Now our example seems a bit more complex, we've got an extra object `props` and every time we want to get a property, we'll actually be implicitly calling the getter trap. Shouldn't this make our code actually much slower than before? Well, turns out here most Javascript engines are actually smart and _inline_ our function.
When a function is _inlined_, it's basically removed, and replaced with its contents. Javascript will essentially optimize our code to something like this:
```js
class Car{
  constructor(definition, color){
    this.props = definition
    this.position = 0
    this.color = color
  }
  //These 2 functions are "inlined"
  //get topSpeed(){return this.props.topSpeed}
  //get name(){return this.props.name}
}
const HondaCivic = {name: 'Honda Civic', topSpeed: 110}

let firstCar = new Car(HondaCivic, 'red')

let _this = firstCar
//Note how our getter access was replaced with the actual function body
console.log((_this.props.topSpeed) == 110)
```

Typically, the engine will decide whether or not a function can be inlined depending on how big it is, and what variables it accesses. After some testing, I could confirm that all major browsers will inline our little getter. :+1:

# Final test
### Typical approach
```js
class Car{
  constructor(color){
    this.position = 0
    this.color = color
  }
}
class HondaCivic extends Car{}
HondaCivic.prototype.topSpeed = 110
HondaCivic.prototype.name = 'Honda Civic'
class FordMondeo extends Car{}
FordMondeo.prototype.topSpeed = 120
FordMondeo.prototype.name = 'Ford Mondeo'
class BmwM4 extends Car{}
BmwM4.prototype.topSpeed = 130
BmwM4.prototype.name = 'BMW M4'
class Lamborghini extends Car{}
Lamborghini.prototype.topSpeed = 160
Lamborghini.prototype.name = 'Lamborghini'
class AnotherCar extends Car{}
AnotherCar.prototype.topSpeed = 999
AnotherCar.prototype.name = 'run out of ideas'

let firstCar = new HondaCivic('red')

function testCar(car, trackLength){
  let seconds = 0
  //run car around a 100km track while counting seconds
  while(car.position < trackLength){
    car.position += car.topSpeed
    seconds++
  }
  return seconds
}

testCar(firstCar, 100000)
testCar(new FordMondeo('grey'), 100000)
testCar(new BmwM4('green'), 100000)
testCar(new Lamborghini('blue'), 100000)
testCar(new AnotherCar('white'), 100000)
```
Test case:
```js
testCar(firstCar,100000)
```
Result `110-111 million ops/s`

### Our solution
```js
class Car{
  constructor(definition, color){
  	this.props = definition
    this.position = 0
    this.color = color
  }
  get topSpeed(){return this.props.topSpeed}
  get name(){return this.props.name}
}
const HondaCivic = {topSpeed: 110, name: 'Honda Civic'}
const FordMondeo = {topSpeed: 120, name: 'Ford Mondeo'}
const BmwM4 = {topSpeed: 130, name: 'BMW M4'}
const Lamborghini = {topSpeed: 160, name: 'Lamborghini'}
const AnotherCar = {topSpeed: 999, name: 'run out of ideas'}


let firstCar = new Car(HondaCivic, 'red')

function testCar(car, trackLength){
  let seconds = 0
  //run car around a 100km track while counting seconds
  while(car.position < trackLength){
    car.position += car.topSpeed
    seconds++
  }
  return seconds
}

testCar(firstCar, 100000)
testCar(new Car(FordMondeo, 'grey'), 100000)
testCar(new Car(BmwM4, 'green'), 100000)
testCar(new Car(Lamborghini, 'blue'), 100000)
testCar(new Car(AnotherCar, 'white'), 100000)
```
Test (same):
```js
testCar(firstCar,100000)
```
Result: `156 million ops/s`