2

I have a question about the import statement that is defined in ES6. Using ES6 import function, you can define the given an object which has been exported from the module either implicitly (because the entire module is exported) or explicitly (using the export statement). I was wondering if there was a memory benefit to using the explicit export. Will it only load those modules into memory or will it load the entire module into memory, and just give you access to the defined modules?

import {foo, bar} from '/modules/my-module.js';
foo();
bar();
// vs
import module from '/modules/my-module.js';
module.foo();
module.bar();
// This can apply to require as well
const {foo, bar} = require('/modules/my-module.js');
foo();
bar();
// vs
const module = require('/modules/my-module.js');
module.foo();
module.bar();
AJ_
  • 3,787
  • 10
  • 47
  • 82
  • FYI - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import – Rahul Desai Apr 08 '18 at 16:31
  • I can't answer this question with 100% certainty, but I think your wording is maybe a little imprecise. I would guess that what's critical is what is _imported_ not what is _exported_. Minimizing the scope of the _import_ by picking specific functions or attributes from the exported interface probably uses less memory. – Matt Morgan Apr 08 '18 at 16:32
  • 1
    Note that `import` and `require()` are **very** different things. Suggest asking about one or the other. – T.J. Crowder Apr 08 '18 at 16:35
  • What implementation are you referring to? It may be different for native, polyfilled and bundled modules. Was es6-module-loader tag chosen randomly? – Estus Flask Apr 08 '18 at 16:37
  • 2
    Also note that `import module from "path";` imports the *default export*, if any, not the whole module. If you want the whole module, you'd use `import * as module from "path";`. – T.J. Crowder Apr 08 '18 at 16:38
  • Will come back later with a proper answer (have to disappear), but for ES2015 modules, the full module is loaded per spec. But if an implementation does "tree shaking", parts of that module could be GC'd fairly quickly if they haven't been imported. So there's benefit, potentially, to only importing what you need. – T.J. Crowder Apr 08 '18 at 16:45
  • @T.J.Crowder My apologizes. I refer to ES6 native, though knowing the answer for bundled modules as well would be good to know, but this question is mainly for ES6 native.. – AJ_ Apr 08 '18 at 16:49
  • To add to TJ's most previous comment (yes, the entire file is loaded by the browser), this can be seen in Paul Irish's demo ToDo app that implements modules natively. Go to the site, look at the helpers.js file in the resource list. You will see the entire file. Then look at what he imports in the bootstrap.js file ($on): https://paulirish.github.io/es-modules-todomvc/ – Randy Casburn Apr 08 '18 at 16:50

1 Answers1

3

Answering for ES2015 modules (import and export, not require()):

TL;DR - The whole module is loaded per spec, but an implementation (or bundler, if you use one) may be able to optimize when it can reliably determine that it can do so without violating the spec. (Whether it does so is another question.) Amongst other things, that means your code would need to avoid eval or new Function in code that can touch the imported module binding, and not use dynamic property access on it. (E.g., mod.foo would be fine, but mod[name] where name is determined at runtime would not.) Personally, I lean toward importing what I need.

A bit more detail:

Let's say we have module1.js:

import { a } from "./module2.js";

function main() {
    a();
}

main();

...and module2.js:

export function a() {
    console.log("a called");
}
export function b() {
    return c() * 2;
}
function c() {
    return 42;
}

If module1 is the starting point, it goes roughly like this:

  • module1.js's source text is parsed and its imports and exports are identified and used to fill in (conceptual) objects representing its live bindings for imports and exports; the values of those bindings are not initialized yet.
  • Since it imports from module2, the same thing is done for module2.js.
  • Since that's the full graph, a depth-first evaluation process starts evaluating modules. Our deepest module is module2, so module2.js's source text is evaluated (run), initializing its export bindings (and the other private things it creates and fills in).
  • Then, all its dependencies satisfied, the same is done for module1.js.

(It gets complicated for cyclic-dependencies, let's leave those aside.)

Remember that bindings are live, so if (for instance) module2 changed the value of a, that change would be reflected in the binding that module1 is using.

So at this point, both modules have been fully loaded and initialized, and that's where the spec leaves off.

Subject to adhering to the specification, implementations can do tree-shaking: Identifying things they can prune once the full tree is established, or even things they can avoid creating in the first place (though with JavaScript, static analysis can only take you so far; but avoiding eval, new Function, dynamic property access, and such helps). So in theory, a sufficiently-optimized engine could get rid of module2's b and c since they aren't used by anything (a doesn't reference them and doesn't use eval or new Function, and module1 only uses a), or perhaps even avoid creating them at all.

Some bundlers also do tree-shaking, trying to determine what parts of a module aren't used anywhere and leaving those bits out of the bundled file.

Now suppose you imported "the whole module":

import * as mod2 from "./module2.js";

function main() {
    mod2.a();
}

main();

I haven't gotten down-and-dirty with the details of JavaScript tree-shaking, but my sense is you've made life more difficult for a bundler or JavaScript engine to do tree-shaking, because mod2 has a property referencing b and it has to prove that you never use it.

Note that if you really want the prefix, you can always rename on import:

import { a as mod2_a } from "./module2.js";

...though granted it's a lot more verbose.

There's been some talk on es-discuss about adding further syntax to import to simplify that (though not a lot so far, I think the focus in this area is on import() and import.meta for now), so perhaps eventually you'll be able to do something where you list the things you want and a pseudo-object to put them on.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Using the namespace is fine: https://stackoverflow.com/a/45746950/785065 – loganfsmyth Apr 08 '18 at 19:25
  • @loganfsmyth: It would be nice to have something backing up Bergi's answer there, which lacks citations. Given who you are, do you have your own experience of implementing tree-shaking to suggest that `import * as` doesn't make it more difficult? (Provided you don't use dynamic property access or similar on the object, since that would make it impossible.) – T.J. Crowder Apr 09 '18 at 06:22
  • Fair point. I unfortunately haven't spent any time looking at existing implementation logic itself, just done enough to know the user-facing behavior. I don't know specifically how Webpack and Rollup decide what is used and how well they handle escape analysis and such for the import object. – loganfsmyth Apr 09 '18 at 07:18
  • @loganfsmyth: Okay, thanks -- was kind of hoping you'd had to get into that area. :-) – T.J. Crowder Apr 09 '18 at 07:23