Skip to content

Cost of inline modules #6346

@dodomorandi

Description

@dodomorandi

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions