question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Lettable operators: scope hoisting prevents dead code removal

See original GitHub issue

RxJS version: 5.5.0

Code to reproduce:

webpack.config.js:

const rxPaths = require("rxjs/_esm5/path-mapping");
const webpack = require("webpack");
const path = require("path");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");

module.exports = {
    entry: "./entry.js",
    output: {
        path: path.resolve("./dist"),
        filename: "[name].bundle.js"
    },
    resolve: {
        alias: rxPaths("./node_modules")
    },
    plugins: [
        new webpack.optimize.ModuleConcatenationPlugin(), // enable scope hoisting
        new UglifyJSPlugin({
            parallel: true,
            uglifyOptions: {
                ecma: 5,
                compress: {
                    passes: 3
                }
            }
        })
    ]
};

entry.js:

import { map } from "rxjs/operators";

// do something with `map`

The above is a variation of the simple config in the lettable operators docs, modified to use webpack’s new UglifyJS plugin.

When you use this config with code that imports from rxjs/operators, it will result in every operator being included in the final bundle whether or not it’s being used. But if you comment out the ModuleConcatenationPlugin line, the unused operators will be properly removed from the output bundle.

This is happening because the ModuleConcatenationPlugin will move the re-exports in rxjs/operators/index.js into the same scope as the actual operator definitions, which prevents webpack from identifying whether those re-exports are actually being used.

Bundle output snippet with scope hoisting disabled:

/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__audit__ = __webpack_require__(40);
/* unused harmony reexport audit */
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__auditTime__ = __webpack_require__(75);
/* unused harmony reexport auditTime */

Bundle output snippet with scope hoisting enabled:

/* unused concated harmony import audit */
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, false, function() { return audit; });
/* unused concated harmony import auditTime */
/* concated harmony reexport */__webpack_require__.d(__webpack_exports__, false, function() { return auditTime; });

Note the difference in unused harmony reexport audit vs concated harmony reexport; it’s this difference that prevents UglifyJS from removing the unused operators in the latter case.

Recommendation:

This is not an rxjs issue, but I thought it might be useful to document the behaviour here and suggest removing ModuleConcatenationPlugin from the example webpack config. Or maybe acknowledging that scope hoisting implies a tradeoff of larger bundle size (when importing from rxjs/operators) vs. smaller parsing/resolving time.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:2
  • Comments:7 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
benleshcommented, Oct 2, 2019

I don’t believe this is an issue any longer with newer bundlers. Closing for now.

0reactions
dairyisscarycommented, Jan 9, 2018

I followed the great example of @jgoz and @ptitjes, but in my particular use case, I had trouble; I would get errors where webpack would not be able to resolve the transformed operator, even though it was present in the resolve alias.

I think its because import { mapTo } from 'rxjs/operators'; (transformed to import { mapTo } from 'rxjs/operators/mapTo'; matched both rxjs/operators and rxjs/operators/mapTo. I just did something like this to make it be an exact match (see webpack resolve documentation):

const rxPathsFactory = require("rxjs/_esm5/path-mapping");

const rxPaths = Object
  .entries(rxPathsFactory())
  .reduce((accum, [ rxImport, realPath ]) => {
    // We need these to be excat matches, so use the dollar at the end.
    return Object.assign(accum, { [`${rxImport}$`]: realPath });
  }, {});

I’m not really sure why this is not the default/what comes out of the path mapping itself.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Scope hoisting - Parcel
This is called "tree shaking" or "dead code elimination". ... Parcel's implementation of scope hoisting works by analyzing each module independently and in ......
Read more >
API - ESBuild
Dead -code elimination within function bodies; Function inlining ... function expression to prevent variables from leaking into the global scope.
Read more >
JavaScript Hoisting - W3Schools
Hoisting is JavaScript's default behavior of moving all declarations to the top of the current scope (to the top of the current script...
Read more >
Untitled
Manfaat manajemen rantai suplai, Code postal maxey sur vaise, ... Laycox collision, 3l patrol to 4.2 conversion, How to remove labret nose piercing, ......
Read more >
ypB - ALBA.Net
Feldkirchen de, Candida en agar sangre, Depressed person synonyms, Code 10 ... Draperi till garderob, Fungating breast lump, Hoisting and rigging course, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found