Skip to content

Optimise __extends #9925

Closed
Closed
@Rycochet

Description

@Rycochet

I was trying to find places to optimise a large project, and realised that the longest running function was actually the __extends call, which prompted me to look at the current code.

Current code

Two things immediately jumped out at me with the current code:

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};

Problems

Potentially unused code (pre ES5)

The first problem was that the __() function was being created, when it might not be used (though chances of that are probably very small).

The Solution would be to move the check into an if/else. Related to this is that null doesn't have any properties, so the property copying should be in the "not null" section.

var __extends = (this && this.__extends) || function (d, b) {
    if (b===null) {
        d.prototype = Object.create(b);
    } else {
        for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
        var __=function() { this.constructor = d; };
        __.prototype = b.prototype;
        d.prototype = new __();
    }
};

Inefficient iteration (for ES5+)

The second problem is purely performance related with the property copying. for(var p in b) will iterate over the entire prototype tree, but if(b.hasOwnProperty(p)) will then skip any that aren't directly on the object/class itself. (see Enumerability and ownership of properties)

The solution would be to use Object.keys(b) and iterating an array, which will work only on the object/class itself (and not it's prototype tree), it also automatically skips the same keys we'd be skipping anyway - so taking most of what we're wanting to the browser / native code instead of JS.

Unlike for...in, Object.keys fails when called on null, undefined or any other non-Object, so this requires some extra code to help prevent that happening.

From here we see that Object.keys() is an ES5+ function.

Suggested code:

var __extends = (this && this.__extends) || function (d, b) {
    if (b instanceof Object) {
        Object.keys(b).forEach(function(p){
            d[p] = b[p];
        });
        var __=function() { this.constructor = d; };
        __.prototype = b.prototype;
        d.prototype = new __();
    } else {
        d.prototype = Object.create(null);
    }
};

Results

It's very hard to get reliable timings for the code on Chrome Profiling, but on average this looks like roughly a 10% speedup at startup for my project (38 classes in a nested tree up to six deep). There is no major benefit to this, though it does make the code slightly "safer".

The ES5+ code could potentially cause different behaviour if a class tries to inherit from a non-class (such as a number or string), though I'm not sure that is possible to begin with..

Language Feature Checklist

  • Syntactic: No changes as this is a compiler change.
  • Semantic: No changes as the final code does the same thing when used as intended (see Results above).
  • Emit: Output code will change depending on ES version needed, both versions are slightly larger than the original.
  • Compatibility: Two versions, one for pre-ES5, most efficient for ES5+
  • Other: There is already an option to prevent __extends being output (which only happens if needed anyway). This could potentially hit older build scripts that manually search & replace the old code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs More InfoThe issue still hasn't been fully clarifiedSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions