Conditional compile/require using browserify (dead code elimination)

775 views Asked by At

I know you can't conditionally require a module with browserify because they're bundled at compile time, not run time. How about conditionally stripping modules?

Let's say I have an app that allows you to create image galleries. The galleries can also be edited (reordering images etc.). But the rendering of the gallery and the editing are coupled and can't be fully separated. But for deploying the galleries I don't need the editing functionality and I know which modules are used. I want to create two different bundles, one with editing capabilities and one without by eliminating most of the editing code. What I did is use envify and uglify's dead code elimination to exclude my own code from the smaller bundle.

Before (thing.js)

//...some code that only the editor needs...
module.exports = thing;

After (thing.js)

if(process.env.INCLUDE_EDITOR === 'yes') {
    //...some code that only the editor needs...
    module.exports = thing;
}

This works great and the editor bundle is already smaller. And since I know that the other bundle will never use the functionality of thing it's OK to just export nothing and have an empty module.

Now here's the problem. If thing.js requires a module, say pica, it will still be bundled even though nobody uses it after dead code elimination.

Before (thing.js)

var pica = require('pica');
//...some code that uses pica...
module.exports = thing;

After (thing.js)

if(process.env.INCLUDE_EDITOR === 'yes') {
    var pica = require('pica');
    //...some code that uses pica...
    module.exports = thing;
}

To sum it up: my bundle now contains the pica library, but nobody requires it. The code that required it was dead code, but uglify can obviously not understand that it could remove pica completely.

2

There are 2 answers

2
casr On BEST ANSWER

I think what you are after is a transform like uglifyify.

Uglifyify gives you the benefit of applying Uglify's "squeeze" transform on each file before it's included in the bundle, meaning you can remove dead code paths for conditional requires.

Take note that you will still want to run uglifyjs on the resulting output.

0
Prinzhorn On

I think I've found a solution that works well enough. As a bonus it doesn't require touching existing code (e.g. adding the process.env. check). I wrote a browserify transform which, given a list of modules/files, replaces their require call with {}. This way they're completely removed from the resulting bundle. It's just a hack anyway. I know.

Before:

var thing = require('./thing.js');
var pica = require('pica');

After:

var thing = {};
var pica = {};

Using https://www.npmjs.com/package/browserify-transform-tools this was only a few lines of code since it already offers a makeRequireTransform helper. I'm gonna dump the code here and recommend nobody should ever use it unless you know exactly what you're doing.

derequire.js

var path = require('path');
var resolve = require('resolve');
var transformTools = require('browserify-transform-tools');

var options = {
    evaluateArguments: true,
    jsFilesOnly: true
};

var cache = {};
var resolveDerequire = function(moduleName) {
    var fromCache = cache[moduleName];

    if(fromCache) {
        return fromCache;
    }

    return require.resolve(moduleName);
};

var transform = transformTools.makeRequireTransform('derequire', options, function(args, transformOptions, done) {
    var requiredModule = args[0];
    var basedir = path.dirname(transformOptions.file);

    var shouldDerequire = transformOptions.config.modules.some(function(moduleToRequire) {
        try {
            //The normal require which respects NODE_PATH.
            return require.resolve(requiredModule) === resolveDerequire(moduleToRequire);
        } catch(ex1) {
            try {
                //A local require relative to the current file.
                return resolve.sync(requiredModule, {basedir: basedir}) === resolveDerequire(moduleToRequire);
            } catch(ex2) {
                console.error(ex1, ex2);
                return false;
            }
        }
    });

    if(shouldDerequire) {
        done(null, '{}');
    } else {
        done();
    }
});


module.exports = transform;

The package.json config for the transform

"browserify": {
    "transform": [
        "babelify",
        [
            "./derequire.js",
            {
                "modules": [
                    "pica",
                    "exif-js",
                    "./src/thing.js",
                    "components/TextEditor.jsx",
                    "lib/FileUploader.js"
                ]
            }
        ],
        "browserify-css",
        "imgurify",
        "glslify"
    ]
},