parcel-bundler/parcel

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).