Quantifying Default Exports

Kevin Smith zenparsing at gmail.com
Mon Jul 14 19:17:44 PDT 2014


In the "ModuleImport" thread, I pointed out that the user experience gains
from default exports would be marginal.  I thought it might be interesting
to put some numbers behind that claim, so I performed a rough analysis on a
sampling of NPM packages.

## Methodology ##

I compiled a list of the 500 "most-dependended-upon" NPM packages using the
data found at https://www.npmjs.org/browse/depended .  For each package, I
downloaded the package archive from the NPM registry, used `npm install` to
install the package's dependencies, and then loaded the package using
Node's `require`.  For each loaded package, I recorded the following
information:

1. The "typeof" value for the loaded module object.
2. For modules reporting `typeof === "object"`, whether the object's
prototype was Object.prototype.
3. For modules reporting `typeof !== "object"`, a list of the object's own
keys.

The installation and loading process failed for a small selection of
packages (usually because of binary compilation issues).  In those cases, I
opened the package code in a text editor and manually recorded the data.

In addition, I performed static analysis on a Python codebase I'm familiar
with containing over 6000 files, counting the number of occurrences of
import declarations with and without import renaming.

### Results ###

a) NPM packages scanned:  500
b) Packages with custom exports (*): 280 (56%)
c) Packages with a function-valued export: 257 (51.4%)
d) Packages with an expanded function export (**): 148 (29.6%)

(*) `typeof exports !== "object || Object.getPrototypeOf(exports) !==
Object.prototype
(**) Exported function has own keys (expandos)

e) Python modules scanned: 6096
f) Imports: 46276
g) Renamed imports: 1472 (3.18%)

### Analysis ###

There are three known arguments supporting default exports.  Let's take a
look at each one in turn.

1) Default exports saves the user the hassle of having to know the exported
name.

This argument is unsound, since the user always has to know the API of the
source module before use, regardless of whether or not default exports are
used.

2) Default exports allows implicit renaming of the imported binding.

>From (b), we see that 56% of the packages in the study export a custom
object, where a custom object is defined as a non-object or an object whose
prototype is not Object.prototype.  It would seem then, that 56% of these
packages would benefit from default exports.

However, we also see that roughly 30% of packages export a function with
custom properties attached as expandos.  These packages are in effect
multiple-export modules.  In ES6, they would be written as named exports
with a default export.  In such cases, the benefit of implicit import
renaming must be balanced against the added API complexity of having both a
default export and named exports:  the user has to remember which exports
are named and which one is anonymous.  We assume that the net effect is no
improvement to user experience.  As such, we can eliminate these cases from
the pool that is expected to benefit from default exports.

That leaves us with 132 packages (26.4% of the total) which will receive a
net benefit from implicit renaming.  Let's call this group B, and its
inverse ~B.  From the Python data (g), we see that imported bindings are
renamed, on average, in only 3.18% of cases.  If we make a simplifying
assumption that packages from B and ~B will be imported on average the same
number of times, we can multiply percentages to find an estimate for the
amount of renames we can expect to save, which turns out to be 0.84%.

Therefore, we can expect to save the user one explicit rename for every 119
imported bindings.

3) Default exports improve interoperability with legacy modules.

All legacy modules which export a custom object will benefit from default
exports, since it will allow the user to write:

    import x from "x";

instead of:

    import { default as x } from "x";

Since 56% of the packages in this study export a custom object, we can
expect this benefit to apply in 56% of cases.  However, two points must be
noted.

First, this is a retrograde optimization.  Optimizations should favor
future usage of the language, not legacy usage.  The value of the
optimization should be viewed in this light.

Secondly, the optimization can be made entirely redundant by simply
modifying the legacy packages in this study to expose their custom export
object as a named expando property.  For example:

    function Foo() {}
    module.exports = Foo;
    Foo.Foo = module.exports;

Then the following will work without supporting syntax:

    import { Foo } from "foo";

Such a strategy will allow users to experience the same benefit as default
exports.

## Conclusion ##

The default export feature creates a two-tiered module system, where a
favored export is made the "default", and subordinate exports are named.
 Such a system is confusing to users and API developers.  From analyzing
NPM packages and import-renaming frequencies in Python, we see that the
optimization provided by default exports for ES6 modules is insignificant.

The optimization for legacy modules is more significant, but there are
other, equally effective solutions which do not require burdening the
language with a confusing two-tiered module system.

It's been suggested that default exports merely paves a cowpath trod by
CommonJS, Node and AMD.  A more appropriate way to view the situation is to
see that ES6 modules (without defaults) builds a highway over the cowpath,
making the cowpath itself obsolete.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20140714/895fc0fa/attachment.html>


More information about the es-discuss mailing list