Deferred re-exports
�export defer … from
Nicolò Ribaudo (Igalia, in partnership with Bloomberg)
April 2025
History
2023-11 Originally presented as part of the import defer proposal
2024-04 Presented more detail about the semantics, and the committee� decided to keep the concept aligned with import defer� (using the same keyword).
2024-06 import defer advanced to Stage 2.7. export defer needed� more work, and got left behind from the main proposal (as� decided in the 2024-04 meeting).
2
Definition: "Barrel files"
3
Barrel files — Current ecosystem practices
The best DX for utility/components libraries is to re-export all their functions through a single entry point:
import { add, debounce, map, ... } from "lodash-es";
and not to ask users to import from their internal files:
import add from "lodash-es/add";
import debounce from "lodash-es/debounce";
import map from "lodash-es/map";
...
4
Barrel files — Current ecosystem practices
How do they support it?
export { default as add } from "./add.js";
export { default as divide } from "./divide.js";
export { default as debounce } from "./debounce.js";
export { default as map } from "./map.js";
export { default as each } from "./each.js";
export { default as some } from "./some.js";
export { default as isObject } from "./isObject.js";
...
5
Barrel files — The problem
I'm importing tens of modules just for a few that I'm actually going to use.
The DX benefits often make people ignore these problems.
6
Current userland solutions
7
Current userland solution: library-specific transforms
This was more popular in the past, but some libraries provide Babel plugin to, at build-time, replace "grouped" imports to "individual" imports.
8
import { map } from 'lodash'
import { add } from 'lodash/fp'
const addOne = add(1)
map([1, 2, 3], addOne)
import map from 'lodash/map'
import add from 'lodash/fp/add'
const addOne = add(1)
map([1, 2, 3], addOne)
Current userland solution (frontend): Tree Shaking
Bundlers are able to, in some cases, avoid bundling unused exports from a bundle:
Due to JS's dynamism, static analysis either detects too much or too little.
9
Current userland solution (Node.js): getter-exports
When using CommonJS, getter-based module.exports can help pruning unused branches in the module graph:
module.exports = {
get add() { return require("./add").default },
get debounce() { return require("./debounce").default },
// ...
};
const l = require("lodash");
l.add(1, 2); // only lodash/add is imported
10
The proposal
11
Web application example
12
// components-library
export {
Button, PrimaryButton
} from "./Button.js"
export {
Tooltip
} from "./Tooltip.js";
export {
From, Input, Checkbox
} from "./forms/index.js";
// handlers.js
import {
Button
} from "components-library";
export function showSubmit() {
$myApp.appendChild(
Button({ label: "Submit" })
);
}
All this code will slow down the app startup without being necessary
Web application example
13
// components-library
export {
Button, PrimaryButton
} from "./Button.js"
export {
Tooltip
} from "./Tooltip.js";
export {
From, Input, Checkbox
} from "./forms/index.js";
// handlers.js
import defer * as c from "components-library";
export function showSubmit() {
$myApp.appendChild(
c.Button({ label: "Submit" })
);
}
We use import defer, and components-library is only executed when actually needed
There is still a bunch of unnecessary work going on: ./Tooltip.js and ./forms/index.js are not needed at all.
Web application example - simpler
14
// components-library
export {
Button, PrimaryButton
} from "./Button.js"
export {
Tooltip
} from "./Tooltip.js";
export {
From, Input, Checkbox
} from "./forms/index.js";
// app.js
import {
Button
} from "components-library";
$myApp.appendChild(
Button({ label: "Submit" })
);
Ignore the "when" is some code needed, focus on the "if"
./Tooltip.js and ./forms/index.js are not needed at all, can we mark them as not needing execution?
Web application example - simpler
15
// components-library
export defer {
Button, PrimaryButton
} from "./Button.js"
export defer {
Tooltip
} from "./Tooltip.js";
export defer {
From, Input, Checkbox
} from "./forms/index.js";
// app.js
import {
Button
} from "components-library";
$myApp.appendChild(
Button({ label: "Submit" })
);
"only evaluate this export if one of the importer modules needs it"
Web application example - simpler - CommonJS solution
16
// components-library
module.exports = {
get Button() { return require("./Button.js").Button; },
get PrimaryButton() {
return require("./Button.js").PrimaryButton; },
get Tooltip() { return require("./Tooltip.js").Tooltip; },
get From() { return require("./forms/index.js").From; },
get Input() { return require("./forms/index.js").Input; },
get Checkbox() {
return require("./forms/index.js").Checkbox; },
};
// app.js
const {
Button
} = require("components-library");
$myApp.appendChild(
Button({ label: "Submit" })
);
export defer … from
Differently from import defer, re-exports' boundary can also be about if a module is evaluated rather than when
We can avoid loading the unused module altogether.
17
Web application example - simpler
18
// components-library
export defer {
Button, PrimaryButton
} from "./Button.js"
export defer {
Tooltip
} from "./Tooltip.js";
export defer {
From, Input, Checkbox
} from "./forms/index.js";
// app.js
import {
Button
} from "components-library";
$myApp.appendChild(
Button({ label: "Submit" })
);
This would only load&execute app.js, components-library, and components-library/Button.js
// components-library
export defer {
Button, PrimaryButton
} from "./Button.js"
export defer {
Tooltip
} from "./Tooltip.js";
export defer {
From, Input, Checkbox
} from "./forms/index.js";
export defer … from
Same goal as import defer (improve startup performance by reducing unnecessary initialization work), but with different loading/TLA semantics:
19
export defer * from
export defer * from "hello" is not valid: you must always explicitly define the names imported from a module, because they need to be known to decide wether to load it or not.
20
A language-level solution
21
A language-level solution
22
Confirm Stage 2?
Or advance from Stage 0 to Stage 1?
23
New proposal repo: https://github.com/nicolo-ribaudo/proposal-deferred-reexports� * previously living in a branch/PR of the import defer repo
Spex text: https://nicolo-ribaudo.github.io/proposal-deferred-reexports/
* there are a couple TODOs left, but it's mostly complete
Integration semantics with import defer
24
import defer * & export
25
// app.js
import defer * as a from "./mod.js";
later(() => use(a.foo));
// mod.js
export { foo } from "dep-foo";
export { bar } from "dep-bar";
| Load | Execute |
app.js | Yes | Startup |
mod.js | Yes | Later |
dep-foo | Yes | Later |
async deps of dep-foo | Startup | |
dep-bar | Yes | Never |
async deps of dep-bar | Startup |
import {...} & export defer
26
// app.js
import { foo } from "./mod.js";
later(() => use(foo));
// mod.js
export defer { foo } from "dep-foo";
export defer { bar } from "dep-bar";
| Load | Execute |
app.js | Yes | Startup |
mod.js | Yes | Startup |
dep-foo | Yes | Startup |
async deps of dep-foo | ||
dep-bar | No | |
async deps of dep-bar |
import * & export defer
27
// app.js
import * as a from "./mod.js";
later(() => use(a.foo));
// mod.js
export defer { foo } from "dep-foo";
export defer { bar } from "dep-bar";
| Load | Execute |
app.js | Yes | Startup |
mod.js | Yes | Startup |
dep-foo | Yes | Later |
async deps of dep-foo | Startup | |
dep-bar | Yes | Never |
async deps of dep-bar | Startup |
import defer * & export defer
28
// app.js
import defer * as a from "./mod.js";
later(() => use(a.foo));
// mod.js
export defer { foo } from "dep-foo";
export defer { bar } from "dep-bar";
| Load | Execute |
app.js | Yes | Startup |
mod.js | Yes | Later |
dep-foo | Yes | Later |
async deps of dep-foo | Startup | |
dep-bar | Yes | Never |
async deps of dep-bar | Startup |
export defer *
29
export defer * from
export defer * from "hello" is not valid: you must always explicitly define the names imported from a module, because they need to be known to decide wether to load it or not.
30
export defer * as ns from
export defer * as ns from "hello" is currently not valid: it's ambiguous, and could mean either of the following:
… or a combination of the two.� This is not set in stone.
31
import defer * as ns from "hello";
export { ns };
export defer { ns } from "intermediate";
// mod.js
export * as ns from "hello";