Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with side effects in tree shaking code?

I've been trying to learn how to write code that is tree shaking friendly, but have run into a problem with unavoidable side effects that I'm not sure how to deal with.

In one of my modules, I access the global Audio constructor and use it to determine which audio files the browser can play (similar to how Modernizr does it). Whenever I try to tree shake my code, the Audio element and all references to it do not get eliminated, even if I don't import the module in my file.

let audio = new Audio(); // or document.createElement('audio')
let canPlay = {
  ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
  mp3: audio.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
  // ...
};

I understand that code that contains side effects cannot be eliminated, but what I can't find is how to deal with unavoidable side effects. I can't just not access a global object to create an audio element needed to detect feature support. So how do I handle accessing global browser functions/objects (which I do a lot in this library) in a way that is tree shaking friendly and still allows me to eliminate the code?

like image 792
Steven Lambert Avatar asked Feb 26 '19 05:02

Steven Lambert


People also ask

How WebPack detect the dead code that can help in tree shaking?

Tools like webpack will detect dead code and mark it as “unused module” but it won't remove the code. Webpack relies on minifiers to cleanup dead code, one of them is UglifyJS plugin, which will eliminate the dead code from the bundle. It only works with import and export . It won't work with CommonJS require syntax.

What does the term tree shaking mean with regard to JS performance?

Tree shaking is a term commonly used within a JavaScript context to describe the removal of dead code. It relies on the import and export statements to detect if code modules are exported and imported for use between JavaScript files.

Does tree shaking remove unused imports?

Tree shaking dynamic imports Dynamic imports resolve the entire module — with its default and named exports — without tree shaking unused imports.


Video Answer


2 Answers

You could take a page out of Haskell/PureScript's book, and simply restrict yourself from having any side effects occur when you import a module. Instead, you export a thunk that represents the side effect of e.g. getting access to the global Audio element in the user's browser, and parametrize the other functions/values wrt the value that this thunk produces.

Here's what it would look like for your code snippet:

// :: type IO a = () -!-> a

// :: IO Audio
let getAudio = () => new Audio();

// :: Audio -> { [MimeType]: Boolean }
let canPlay = audio => {
  ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
  mp3: audio.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
  // ...
};

Then in your main module you can use the appropriate thunks to instantiate the globals you actually need, and plug them into the parametrized functions/values that consume them.

It's fairly obvious how to plug all these new parameters manually, but it can get tedious. There's several techniques to mitigate this; an approach you can again steal from Haskell/PureScript is to use the reader monad, which facilitates a kind of dependency injection for programs consisting of simple functions.

A much more detailed explanation of the reader monad and how to use it to thread some context throughout your program is beyond the scope of this answer, but here are some links where you can read about these things:

  • https://github.com/monet/monet.js/blob/master/docs/READER.md
  • https://www.youtube.com/embed/ZasXwtTRkio?rel=0
  • https://www.fpcomplete.com/blog/2017/06/readert-design-pattern

(disclaimer: I haven't thoroughly read or vetted all of these links, I just googled keywords and copied some links where the introduction looked promising)

like image 172
Asad Saeeduddin Avatar answered Oct 28 '22 07:10

Asad Saeeduddin


You can implement a module to give you a similar usage pattern that your question suggests, using audio() to access the audio object, and canPlay, without a function call. This can be done by running the Audio constructor in a function, as Asad suggested, and then calling that function every time you wish to access it. For the canPlay, we can use a Proxy, allowing the array indexing to be implemented under the hood as a function.

Let's assume we create a file audio.js:

let audio = () => new Audio();
let canPlay = new Proxy({}, {
    get: (target, name) => {
        switch(name) {
            case 'ogg':
                return audio().canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
            case 'mp3':
                return audio().canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
        }
    }
});

export {audio, canPlay}

These are the results of running on various index.js files, rollup index.js -f iife:

import {} from './audio';
(function () {
    'use strict';



}());
import {audio} from './audio';

console.log(audio());
(function () {
    'use strict';

    let audio = () => new Audio();

    console.log(audio());

}());
import {canPlay} from './audio';

console.log(canPlay['ogg']);
(function () {
    'use strict';

    let audio = () => new Audio();
    let canPlay = new Proxy({}, {
        get: (target, name) => {
            switch(name) {
                case 'ogg':
                    return audio().canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
                case 'mp3':
                    return audio().canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
            }
        }
    });

    console.log(canPlay['ogg']);

}());

Additionally, there is no way to implement audio as originally intended if you wish to preserve the properties outlined in the question. Other short possibilities to audio() are +audio or audio`` (as shown here: Invoking a function without parentheses), which can be considered to be more confusing.

Finally, other global variables that don't involve an array index or function call will have to be implemented in similar ways to let audio = () => new Audio();.

like image 44
Pedro Amaro Avatar answered Oct 28 '22 05:10

Pedro Amaro