Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code after 'break' statement is not removed, before terser plugin. #16480

Closed
icy0307 opened this issue Nov 16, 2022 · 17 comments
Closed

code after 'break' statement is not removed, before terser plugin. #16480

icy0307 opened this issue Nov 16, 2022 · 17 comments

Comments

@icy0307
Copy link

icy0307 commented Nov 16, 2022

Feature request

Thx for this awesome library.
First of all, I don't know is this a bug or just not implemented yet.
This is my mini repo to reproduce this problem. The webpack version I used is 5.38.1
In index.ts, I'm trying to load the dependencies dynamically in order to get small bundle size.

export const treeShakingTestAsync = async () => {
    console.log('treeShakingTestAsync');
    //  Be replaced with 'false' by DefinePlugin
    if (__MOBILE__) {
        const { Baz } = await import(/* webpackChunkName: "bundle_mobile" */'./mobile');
        console.log(Baz);
    } else {
        const { foo } = await import(/* webpackChunkName: "bundle_pc" */ './pc');
        console.log(foo);
    }

};

pc.ts and mobile.ts both use some of the code from a common module.

// pc.ts
export { bar , foo } from './lib-modules';
// mobile.ts
export { Baz } from './lib-modules';

lib-modules.ts mimics a large esm package index file.

// lib-modules
export function foo() { console.log('FuncFoo') };
export function bar() {
    console.log('FuncFoo')
};
export class Baz {
    private c = 'ClassBaz'
}

In the webpack config , everything is default, except turning minimizer off to see what webpack does before passing to terser.
and bundle the pseudo package lib-modules in to one bundle.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
  entry: './src/treeshaking-test.ts',
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  optimization: {
    minimize: false,
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
        test: {
          test: /lib-modules/,
          name: 'bundle-lib-modules',
          priority: 101,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [
    new webpack.DefinePlugin({
      __TEST__: true
    }),
    new HtmlWebpackPlugin(),
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
};

The result is perfect, webpack removed the dead code block in bracket after "false" before involvement of SplitchunkPlugin or TerserPlugin.
Baz is not in the output.

/* unused harmony export treeShakingTestAsync */
const treeShakingTestAsync = async () => {
    console.log('treeShakingTestAsync');
    if (false) {}
    else {
        const { foo } = await Promise.all(/* import() | bundle_pc */[__webpack_require__.e(910), __webpack_require__.e(941)]).then(__webpack_require__.bind(__webpack_require__, 422));
        console.log(foo);
    }
};

But when I add a babel-loader after ts-loader for polyfills
the code get transpiled to

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return exports; }; var exports = {}, Op = Object.prototype, hasOwn = Op.hasOwnProperty, defineProperty = Object.defineProperty || function (obj, key, desc) { obj[key] = desc.value; }, $Symbol = "function" == typeof Symbol ? Symbol : {}, iteratorSymbol = $Symbol.iterator || "@@iterator", asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator", toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function define(obj, key, value) { return Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }), obj[key]; } try { define({}, ""); } catch (err) { define = function define(obj, key, value) { return obj[key] = value; }; } function wrap(innerFn, outerFn, self, tryLocsList) { var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator, generator = Object.create(protoGenerator.prototype), context = new Context(tryLocsList || []); return defineProperty(generator, "_invoke", { value: makeInvokeMethod(innerFn, self, context) }), generator; } function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } exports.wrap = wrap; var ContinueSentinel = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var IteratorPrototype = {}; define(IteratorPrototype, iteratorSymbol, function () { return this; }); var getProto = Object.getPrototypeOf, NativeIteratorPrototype = getProto && getProto(getProto(values([]))); NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol) && (IteratorPrototype = NativeIteratorPrototype); var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function (method) { define(prototype, method, function (arg) { return this._invoke(method, arg); }); }); } function AsyncIterator(generator, PromiseImpl) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if ("throw" !== record.type) { var result = record.arg, value = result.value; return value && "object" == _typeof(value) && hasOwn.call(value, "__await") ? PromiseImpl.resolve(value.__await).then(function (value) { invoke("next", value, resolve, reject); }, function (err) { invoke("throw", err, resolve, reject); }) : PromiseImpl.resolve(value).then(function (unwrapped) { result.value = unwrapped, resolve(result); }, function (error) { return invoke("throw", error, resolve, reject); }); } reject(record.arg); } var previousPromise; defineProperty(this, "_invoke", { value: function value(method, arg) { function callInvokeWithMethodAndArg() { return new PromiseImpl(function (resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); } }); } function makeInvokeMethod(innerFn, self, context) { var state = "suspendedStart"; return function (method, arg) { if ("executing" === state) throw new Error("Generator is already running"); if ("completed" === state) { if ("throw" === method) throw arg; return doneResult(); } for (context.method = method, context.arg = arg;;) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if ("next" === context.method) context.sent = context._sent = context.arg;else if ("throw" === context.method) { if ("suspendedStart" === state) throw state = "completed", context.arg; context.dispatchException(context.arg); } else "return" === context.method && context.abrupt("return", context.arg); state = "executing"; var record = tryCatch(innerFn, self, context); if ("normal" === record.type) { if (state = context.done ? "completed" : "suspendedYield", record.arg === ContinueSentinel) continue; return { value: record.arg, done: context.done }; } "throw" === record.type && (state = "completed", context.method = "throw", context.arg = record.arg); } }; } function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (undefined === method) { if (context.delegate = null, "throw" === context.method) { if (delegate.iterator.return && (context.method = "return", context.arg = undefined, maybeInvokeDelegate(delegate, context), "throw" === context.method)) return ContinueSentinel; context.method = "throw", context.arg = new TypeError("The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if ("throw" === record.type) return context.method = "throw", context.arg = record.arg, context.delegate = null, ContinueSentinel; var info = record.arg; return info ? info.done ? (context[delegate.resultName] = info.value, context.next = delegate.nextLoc, "return" !== context.method && (context.method = "next", context.arg = undefined), context.delegate = null, ContinueSentinel) : info : (context.method = "throw", context.arg = new TypeError("iterator result is not an object"), context.delegate = null, ContinueSentinel); } function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; 1 in locs && (entry.catchLoc = locs[1]), 2 in locs && (entry.finallyLoc = locs[2], entry.afterLoc = locs[3]), this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal", delete record.arg, entry.completion = record; } function Context(tryLocsList) { this.tryEntries = [{ tryLoc: "root" }], tryLocsList.forEach(pushTryEntry, this), this.reset(!0); } function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) return iteratorMethod.call(iterable); if ("function" == typeof iterable.next) return iterable; if (!isNaN(iterable.length)) { var i = -1, next = function next() { for (; ++i < iterable.length;) { if (hasOwn.call(iterable, i)) return next.value = iterable[i], next.done = !1, next; } return next.value = undefined, next.done = !0, next; }; return next.next = next; } } return { next: doneResult }; } function doneResult() { return { value: undefined, done: !0 }; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, defineProperty(Gp, "constructor", { value: GeneratorFunctionPrototype, configurable: !0 }), defineProperty(GeneratorFunctionPrototype, "constructor", { value: GeneratorFunction, configurable: !0 }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction"), exports.isGeneratorFunction = function (genFun) { var ctor = "function" == typeof genFun && genFun.constructor; return !!ctor && (ctor === GeneratorFunction || "GeneratorFunction" === (ctor.displayName || ctor.name)); }, exports.mark = function (genFun) { return Object.setPrototypeOf ? Object.setPrototypeOf(genFun, GeneratorFunctionPrototype) : (genFun.__proto__ = GeneratorFunctionPrototype, define(genFun, toStringTagSymbol, "GeneratorFunction")), genFun.prototype = Object.create(Gp), genFun; }, exports.awrap = function (arg) { return { __await: arg }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, asyncIteratorSymbol, function () { return this; }), exports.AsyncIterator = AsyncIterator, exports.async = function (innerFn, outerFn, self, tryLocsList, PromiseImpl) { void 0 === PromiseImpl && (PromiseImpl = Promise); var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList), PromiseImpl); return exports.isGeneratorFunction(outerFn) ? iter : iter.next().then(function (result) { return result.done ? result.value : iter.next(); }); }, defineIteratorMethods(Gp), define(Gp, toStringTagSymbol, "Generator"), define(Gp, iteratorSymbol, function () { return this; }), define(Gp, "toString", function () { return "[object Generator]"; }), exports.keys = function (val) { var object = Object(val), keys = []; for (var key in object) { keys.push(key); } return keys.reverse(), function next() { for (; keys.length;) { var key = keys.pop(); if (key in object) return next.value = key, next.done = !1, next; } return next.done = !0, next; }; }, exports.values = values, Context.prototype = { constructor: Context, reset: function reset(skipTempReset) { if (this.prev = 0, this.next = 0, this.sent = this._sent = undefined, this.done = !1, this.delegate = null, this.method = "next", this.arg = undefined, this.tryEntries.forEach(resetTryEntry), !skipTempReset) for (var name in this) { "t" === name.charAt(0) && hasOwn.call(this, name) && !isNaN(+name.slice(1)) && (this[name] = undefined); } }, stop: function stop() { this.done = !0; var rootRecord = this.tryEntries[0].completion; if ("throw" === rootRecord.type) throw rootRecord.arg; return this.rval; }, dispatchException: function dispatchException(exception) { if (this.done) throw exception; var context = this; function handle(loc, caught) { return record.type = "throw", record.arg = exception, context.next = loc, caught && (context.method = "next", context.arg = undefined), !!caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i], record = entry.completion; if ("root" === entry.tryLoc) return handle("end"); if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"), hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } else if (hasCatch) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); } else { if (!hasFinally) throw new Error("try statement without catch or finally"); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } } } }, abrupt: function abrupt(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } finallyEntry && ("break" === type || "continue" === type) && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc && (finallyEntry = null); var record = finallyEntry ? finallyEntry.completion : {}; return record.type = type, record.arg = arg, finallyEntry ? (this.method = "next", this.next = finallyEntry.finallyLoc, ContinueSentinel) : this.complete(record); }, complete: function complete(record, afterLoc) { if ("throw" === record.type) throw record.arg; return "break" === record.type || "continue" === record.type ? this.next = record.arg : "return" === record.type ? (this.rval = this.arg = record.arg, this.method = "return", this.next = "end") : "normal" === record.type && afterLoc && (this.next = afterLoc), ContinueSentinel; }, finish: function finish(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) return this.complete(entry.completion, entry.afterLoc), resetTryEntry(entry), ContinueSentinel; } }, catch: function _catch(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if ("throw" === record.type) { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(iterable, resultName, nextLoc) { return this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }, "next" === this.method && (this.arg = undefined), ContinueSentinel; } }, exports; }
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
var treeShakingTestAsync = /*#__PURE__*/(/* unused pure expression or super */ null && (function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
    var _yield$import, Baz, _yield$import2, foo;
    return _regeneratorRuntime().wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            console.log('treeShakingTestAsync');
            if (true) {
              _context.next = 9;
              break;
            }
            _context.next = 4;
            return Promise.all(/* import() | bundle_mobile */[__webpack_require__.e(910), __webpack_require__.e(593)]).then(__webpack_require__.bind(__webpack_require__, 657));
          case 4:
            _yield$import = _context.sent;
            Baz = _yield$import.Baz;
            console.log(Baz);
            _context.next = 14;
            break;
          case 9:
            _context.next = 11;
            return Promise.all(/* import() | bundle_pc */[__webpack_require__.e(910), __webpack_require__.e(941)]).then(__webpack_require__.bind(__webpack_require__, 395));
          case 11:
            _yield$import2 = _context.sent;
            foo = _yield$import2.foo;
            console.log(foo);
          case 14:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return function treeShakingTestAsync() {
    return _ref.apply(this, arguments);
  };
}()));

Although, case 4 can never be reached, it still remains in the source code, cause module mobile and its dependencies being bundled, which leads to huge bundle size in real world.

What is the expected behavior?
Baz is not in the output

What is motivation or use case for adding/changing the behavior?
smaller bundle size
How should this be implemented in your opinion?
There is an export part in the vendors bundle.

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "Kb": () => (/* binding */ bar),
/* harmony export */   "Mu": () => (/* binding */ Baz),
/* harmony export */   "RN": () => (/* binding */ foo)
/* harmony export */ });

Clearly, dead code removal would not being working. webpack seems removed all code block after the 'false' statement before compilation 'sealed'. Could you do the same for 'break' statement that never could be reached?
@sokra @alexander-akait
Are you willing to work on this yourself?
yes
But I am not familiar with webpack's compilation process, is there any docs could help?
Also I also wondering , there seems be a nuance in 'tree-shaking'(things not be in included, or 'rolled up') and 'dead code removal', Is there any doc elaborate on this subject?

@alexander-akait
Copy link
Member

I think the best place for it is terser repo, but it is hard to implement (dce is the most diffucult part), that is why you faced with it and looks like terser is not implement it, try to rewrite:

async function foo() { 
        const { Baz } = await import(/* webpackChunkName: "bundle_mobile" */'./mobile');
        console.log(Baz);
}

async function bar() { 
       const { foo } = await import(/* webpackChunkName: "bundle_pc" */ './pc');
        console.log(foo);
}


export const treeShakingTestAsync = async () => {
    console.log('treeShakingTestAsync');
    //  Be replaced with 'false' by DefinePlugin
    if (__MOBILE__) {
        await foo();
    } else {
       await bar();
    }
};

@icy0307
Copy link
Author

icy0307 commented Nov 17, 2022

Thanks for you reply.@alexander-akait . I test async import before. It seems it could not be shaken #16271 #14800.
May I ask why do you think the best place for it is terser repo? Webpack seems already has done some elimination work before let terser handles it. For example , for the original code I posted, all code after "false" is already removed, so chunk "bundle_mobile" not be added to chunk graph in the first place. Isn't get removed on purpose? Does splitchunk happens before terser? Even terser plugin can eliminate it, wouldn't it be too late, cause "Baz" is already added to binding part in the vendors chunk and terser plugin only works on chunk level. Why some code is already removed before terser? Why code after "break" statement cannot be part of it?

@alexander-akait
Copy link
Member

alexander-akait commented Nov 17, 2022

Because it is DCE (dead code elimination), it is not a part of webpack, webpack just says false/undefined/etc or remove usage (+ import/export) and then terser remove everything which was not used, i.e. do DCE

@alexander-akait
Copy link
Member

#14800 is for import destrucation and remove unsed imports

@icy0307
Copy link
Author

icy0307 commented Nov 17, 2022

I think the best place for it is terser repo, but it is hard to implement (dce is the most diffucult part), that is why you faced with it and looks like terser is not implement it, try to rewrite:

async function foo() { 
        const { Baz } = await import(/* webpackChunkName: "bundle_mobile" */'./mobile');
        console.log(Baz);
}

async function bar() { 
       const { foo } = await import(/* webpackChunkName: "bundle_pc" */ './pc');
        console.log(foo);
}


export const treeShakingTestAsync = async () => {
    console.log('treeShakingTestAsync');
    //  Be replaced with 'false' by DefinePlugin
    if (__MOBILE__) {
        await foo();
    } else {
       await bar();
    }
};

I changed the demo into the code above
which cause the entry function became this.(skip all irrelevant part)

var treeShakingTestAsync = /*#__PURE__*/(/* unused pure expression or super */ null && (function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
    return _regeneratorRuntime().wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            console.log('treeShakingTestAsync');
            if (true) {
              _context.next = 6;
              break;
            }
            _context.next = 4;
            return foo();
          case 4:
            _context.next = 8;
            break;
          case 6:
            _context.next = 8;
            return bar();
          case 8:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return function treeShakingTestAsync() {
    return _ref.apply(this, arguments);
  };

As you can see, both foo and bar still remains

  return _foo.apply(this, arguments);
}
function _foo() {
  _foo = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2() {
    var _yield$import, Baz;
    return _regeneratorRuntime().wrap(function _callee2$(_context2) {
      while (1) {
        switch (_context2.prev = _context2.next) {
          case 0:
            _context2.next = 2;
            return Promise.all(/* import() | bundle_mobile */[__webpack_require__.e(910), __webpack_require__.e(593)]).then(__webpack_require__.bind(__webpack_require__, 657));
          case 2:
            _yield$import = _context2.sent;
            Baz = _yield$import.Baz;
            console.log(Baz);
          case 5:
          case "end":
            return _context2.stop();
        }
      }
    }, _callee2);
  }));
  return _foo.apply(this, arguments);
}
function bar() {
  return _bar.apply(this, arguments);
}
function _bar() {
  _bar = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3() {
    var _yield$import2, foo;
    return _regeneratorRuntime().wrap(function _callee3$(_context3) {
      while (1) {
        switch (_context3.prev = _context3.next) {
          case 0:
            _context3.next = 2;
            return Promise.all(/* import() | bundle_pc */[__webpack_require__.e(910), __webpack_require__.e(941)]).then(__webpack_require__.bind(__webpack_require__, 395));
          case 2:
            _yield$import2 = _context3.sent;
            foo = _yield$import2.foo;
            console.log(foo);
          case 5:
          case "end":
            return _context3.stop();
        }
      }
    }, _callee3);
  }));
  return _bar.apply(this, arguments);
}

which lead to bundle_mobile still exists and Baz still get bundled in 'bundle-lib-modules'

@alexander-akait
Copy link
Member

Yeah, code is complex, seems like it is impossible, can you try to switch on swc instead terser - https://github.com/webpack-contrib/terser-webpack-plugin#swc, just for tests

@icy0307
Copy link
Author

icy0307 commented Nov 17, 2022

Sorry for not being able to express myself more clearly(not a native speaker).
If I understand it correctly

  1. bundle_mobile is added to the chunk graph first

image

2. then splitchunk takes place,

image

which cause 'bundle-lib-modules'(the vendors chunk contains `Baz`) being added.

企业微信截图_ebd43a5b-973a-478b-bac0-9ac40922e218

3. Terser happens last , in chunk level. So terser or any other minimize tool **cannot do anything**, cause `Baz` is reference by the "binding" part.
"use strict";
(self["webpackChunkgetting_started_using_a_configuration"] = self["webpackChunkgetting_started_using_a_configuration"] || []).push([[910],{

/***/ 253:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "Kb": () => (/* binding */ bar),
/* harmony export */   "Mu": () => (/* binding */ Baz),
/* harmony export */   "RN": () => (/* binding */ foo)
/* harmony export */ });
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function foo() {
  console.log('FuncFoo');
}
;
function bar() {
  console.log('FuncFoo');
}
;
var Baz = /*#__PURE__*/_createClass(function Baz() {
  _classCallCheck(this, Baz);
  this.c = 'ClassBaz';
});

/***/ })

}]);

@alexander-akait
Copy link
Member

@icy0307 Do you use foo or bar in other places? Maybe you can create small repo and I will look

@icy0307
Copy link
Author

icy0307 commented Nov 17, 2022

https://github.com/icy0307/webpack-mini-repo @alexander-akait
No foo or bar and Baz is being used any other places

@icy0307
Copy link
Author

icy0307 commented Nov 17, 2022

Because it is DCE (dead code elimination), it is not a part of webpack, webpack just says false/undefined/etc or remove usage (+ import/export) and then terser remove everything which was not used, i.e. do DCE

You can see in my repo , I turned off 'minimize' completely, anything in the comes after the 'false' if statement, is already been removed before. (I even add a debugger in terser's entry , it has not been called if minimizer has been turned off)

var __webpack_exports__ = {};
/* unused harmony export treeShakingTestAsync */
const treeShakingTestAsync = async () => {
    console.log('treeShakingTestAsync');
    // @ts-ignore
    if (false) {}
    else {
        const { foo } = await Promise.all(/* import() | bundle_pc */[__webpack_require__.e(910), __webpack_require__.e(941)]).then(__webpack_require__.bind(__webpack_require__, 422));
        console.log(foo);
    }
};

So I guess this removal part is not performed by minimizer. Am I misunderstanding something? @alexander-akait

@alexander-akait
Copy link
Member

Sorry it is out of scope webpack at all, I said above, after babel you have:

var treeShakingTestAsync = /*#__PURE__*/(/* unused pure expression or super */ null && (function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
    var _yield$import, Baz, _yield$import2, _foo;
    return _regeneratorRuntime().wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            console.log('treeShakingTestAsync');
            // @ts-ignore
            if (!foo) {
              _context.next = 9;
              break;
            }
            _context.next = 4;
            return Promise.all(/* import() | bundle_mobile */[__webpack_require__.e(910), __webpack_require__.e(593)]).then(__webpack_require__.bind(__webpack_require__, 657));
          case 4:
            _yield$import = _context.sent;
            Baz = _yield$import.Baz;
            console.log(Baz);
            _context.next = 14;
            break;
          case 9:
            _context.next = 11;
            return Promise.all(/* import() | bundle_pc */[__webpack_require__.e(910), __webpack_require__.e(941)]).then(__webpack_require__.bind(__webpack_require__, 395));
          case 11:
            _yield$import2 = _context.sent;
            _foo = _yield$import2.foo;
            console.log(_foo);
          case 14:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return function treeShakingTestAsync() {
    return _ref.apply(this, arguments);
  };
}()));

i.e.

// @ts-ignore
if (!__MOBILE__) {
  _context.next = 9;
  break;
}

if you set __MOBILE__: false it will be removed by webpack.

We handle code after loaders, and code after babel is complex and is not optimized for evalator (this is static analizator to check expressions and other code on false/undefined/etc and drop them, it is not design for wokring with dynamic stuff - i.e. variables/closures/ etc) to undestand unused exprenssions, it is AST based optimization, here is very complex optimization with dynamic stuff (i.e. _context) and should be handle by minifier or other tools.

Solutions:

  1. AST terser/swc/etc minifiers to implement such complex optimization
  2. ask babel to change code generation, here (Implement transform-async-to-promises babel/babel#7076) is a intresing idea to change it on Promise
  3. Change your code on (i.e. no babel complex output)
export const treeShakingTestAsync = () => {
    console.log('treeShakingTestAsync');
    // @ts-ignore
    if (__MOBILE__) {
        Promise.resolve().then(() => import(/* webpackChunkName: "bundle_mobile" */'./mobile')).then((loadded) => {
            console.log(loadded.Baz);
        })

    } else {
        Promise.resolve().then(() => import(/* webpackChunkName: "bundle_pc" */ './pc')).then((loadded) => {
            console.log(loadded.foo);
        })
    }

};

Sorry we can't do something here. Babel output non statical analizable code, so we can't drop more branches as you want, anyway I think babel can optimize own output and avoid generate weird if (true) in such cases

@icy0307
Copy link
Author

icy0307 commented Nov 18, 2022

Thanks for the detailed explanation!

  1. Solution 3 seems to be quick workaround. But I am actually work in a huge code base, even It could be done automatically, I doubt the this solution could be accepted by my team for readability concerns. It basically tells people do not use dynamic import after expression or in generator function in our project.
  2. Terser or any other minifier probably would never implement this due to its complexity. But Even if terser do implement this, terser plugin happens last, isn't Baz already has been bundled?

We handle code after loaders

Would it be possible and rational to run a custom loader before babel loader , simply to drop this code(trunoff mangle just compress)?
And just out of curiosity, where is this webpack's analizator the drop code in source code?

  1. As for solution2 you mentioned aboved. I got some trouble to understand what is behind the scenes. It seems this solution is from performance perspective, But why it is faster this way?

tslib handles the generator the same way babel does. So is adding a custom loader to remove the code in advance a better solution? @alexander-akait

@alexander-akait
Copy link
Member

Would it be possible and rational to run a custom loader before babel loader , simply to drop this code(trunoff mangle just compress)?

No it is impossible, but you can do it, you can keep code in ES latest and use ESM modues and when you need old browser support run babel on webpack generated code, it is good solutions when you need really old browser suppport (or you need something special after bundling), you can create plugin or just use CLI

So tree-shaking will work and you will have support of old browsers.

Sorry, we are statical analizator, you want to handle dynamic case, it is out of scope webpack and require a lot of complex logic (not sure bundle should do it at all).

@icy0307
Copy link
Author

icy0307 commented Nov 21, 2022

Thank you! @alexander-akait
I understand that is it out of scope now.
I just wonder why something like terser-loader is not correct?
Why it is impossible to remove some of the code before any loaders? Why webpack choose to do handle code after loader?

@icy0307
Copy link
Author

icy0307 commented Nov 21, 2022

What I am suggesting is to run terser twice.
The first time as loader that runs before all js loaders, only to drop the code.
The second time as terser plugin to work on bundle level?

The root cause if this problem is webpack do eliminate code before chunks are created(via 'ConstPlugin'?), but some loaders make the optimization volatile. Why can't run code elimination before all javascript loader one more time as well?

@alexander-akait
Copy link
Member

alexander-akait commented Nov 25, 2022

Why can't run code elimination before all javascript loader one more time as well?

Because loader generates extra code and we need handle it - loader can add more import/require, more code, webpack stuff and etc and we should handle it (I think you undestand why we need handle it), we have special syntax to run loader before/after (you can find it in docs), but it will not solve your problem and creates more problems.

@alexander-akait
Copy link
Member

Feel free to feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants