rescript-compiler
rescript-compiler copied to clipboard
Cost of inline modules
I was playing around and I realized that, with the current approach to compile inline modules, we produce JS code that cannot be tree-shaked (at least using esbuild 0.18.13).
// Split.res
module Nested = Split__Nested
// Split__Nested.res
let adder = x => {
x + 2
}
let multiplier = x => {
x * 2
}
// Inlined.res
module Nested = {
let adder = x => {
x + 2
}
let multiplier = x => {
x * 2
}
}
// MainSplit.res
Console.log(Split.Nested.adder(2)->Int.toString)
// MainInlined.res
Console.log(Inlined.Nested.adder(2)->Int.toString)
// MainSplit.resi
// MainInlined.resi
If you compile this (I am currently using rescript 11.0.0-beta.4), then you can use esbuild
to minify and bundle the two main files separatedly (--keep-names
is just to make things a bit more readable):
rescript build
esbuild --minify --bundle --keep-names src/MainSplit.bs.js
esbuild --minify --bundle --keep-names src/MainInlined.bs.js
This is the formatted output for MainSplit
:
(() => {
var i = Object.defineProperty;
var e = (t, o) => i(t, "name", { value: o, configurable: !0 });
function r(t) {
return (t + 2) | 0;
}
e(r, "adder");
console.log(r(2).toString());
})();
This is the output for MainInlined
instead:
(() => {
var d = Object.defineProperty;
var r = (e, n) => d(e, "name", { value: n, configurable: !0 });
function i(e) {
return (e + 2) | 0;
}
r(i, "adder");
function o(e) {
return e << 1;
}
r(o, "multiplier");
var t = { adder: i, multiplier: o };
console.log(t.adder(2).toString());
})();
As you can see, we keep the multiplier
function around in the inlined version, and the reason is pretty simple: because we represent an inline module as an object, it is impossible for the tree-shaker to only remove multiplier
from the object t
.
I hope to be wrong, but I do not think there is a nice and easy solution, just because it is not possible to express the concept of an inline module in JS.
We could simply consider to warn the users that inline modules can lead to bundle size increase, but given that in Rescript modules are first citizen elements (and they are pretty pervasives) maybe we should consider exploring better approaches.
An obvious one could be to automatically split inline modules into separated JS files, but the following situation is totally not trivial to handle:
// Test.res
let a = x => {
x + 10
}
module Inner = {
let b = x => {
a(x) * 2
}
}
// Test.resi
// Note that `a` is not exported
module Inner: {
let b: int => int
}
In fact, a
should be exported to Inner
but not to other modules according to the interface file.
As you can see, I really do not have a good solution to this problem. Probably the situation is made a little worse by the way module files are flat in Rescript. But let me know what you think.