Dead-code elimination of unused default parameters
bitjson opened this issue ยท 2 comments
๐ Feature Request
During dead-code elimination/tree shaking, default parameters that aren't used by the bundle can be removed.
๐ค Expected Behavior
For this simple example:
module.js
:
const internalAdd = (a, b) => (a === '0' ? b : a + b); // should not appear in bundle
export const addAndLog = (a, b, impl = internalAdd) => console.log(impl(a, b));
app.js
:
import { addAndLog } from './module.js';
const myAdd = (a, b) => a + b;
addAndLog(1, 2, myAdd); // => 3
The produced bundle should include only:
const addAndLog = (a, b, impl) => console.log(impl(a, b));
const myAdd = (a, b) => a + b;
addAndLog(1, 2, myAdd); // => 3
๐ฏ Current Behavior
Currently, parcel bundles the dead code:
const internalAdd = (a, b) => (a === '0' ? b : a + b); // should not appear in bundle
const addAndLog = (a, b, impl = internalAdd) => console.log(impl(a, b));
const myAdd = (a, b) => a + b;
addAndLog(1, 2, myAdd); // => 3
๐ฆ Context
I maintain Libauth, a library that offers WebAssembly crypto implementations (ripemd160
, sha1
, sha256
, sha512
, and secp256k1
). As a pure ESM library, Libauth can asynchronously instantiate each WASM implementation internally, exporting simple interfaces that behave like collections of JS-only functions (with better performance). Many of Libauth's exported functions also use one of these built-in WASM instances as a default parameter. For example, decodeBase58Address
has the definition:
export const decodeBase58Address = (
address: string,
sha256: { hash: (input: Uint8Array) => Uint8Array } = internalSha256
) => {
// ...
};
Most applications can call decodeBase58Address(address)
to automatically use the default, WASM-based sha256
implementation (internalSha256
).
However, applications that already have another sha256
implementation can provide that implementation as the second parameter: decodeBase58Address(address, mySha256Implementation)
. In this case, the default parameter (internalSha256
) is dead code, and should be possible to eliminate from the application's bundle, saving hundreds of KBs from those bundles.
๐ป Examples
A detailed example (testing with all known bundlers) is available at this repo: https://github.com/bitjson/shake-default-params
I agree with evanw/esbuild#2185 (comment). The only DCE done by Parcel itself is excluding unused files (if they have sideEffects: false
). Apart from that it just tries to transform as many imports into regular top-level functions calls as possible (and not with a namespace object as i.e. Babel does _module.addAndLog()
). The actual dead code is then removed by Terser/whatever minified you want to use afterwards.
Right now, Terser turns
const internalAdd = (a, b) => "dead";
const addAndLog = (impl = internalAdd) => console.log(impl(1, 2));
addAndLog(3)
into
const o=(o,c)=>"dead";((c=o)=>{console.log(c(1,2))})(3);
Thanks for the info @mischnic! I just opened an issue for terser
: terser/terser#1199
For anyone finding this issue before Parcel/terser support this optimization, Rollup will support tree-shaking default parameters; the PR is here: rollup/rollup#4498).