Skip to content

Regression in main bundle size. #20149

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

Closed
1 task done
SanderElias opened this issue Feb 26, 2021 · 4 comments
Closed
1 task done

Regression in main bundle size. #20149

SanderElias opened this issue Feb 26, 2021 · 4 comments

Comments

@SanderElias
Copy link

SanderElias commented Feb 26, 2021

🐞 Bug report

Command (mark with an x)

  • build

Is this a regression?

Yes, the previous version in which this bug was not present was: ....

Description

When I build my app with version 11.0.7, this is the initial bundle:

Initial Chunk Files Names Size
polyfills.e360d1535f100e4f0e96.js polyfills 50.76 kB
runtime.e3a55ec346089bacde1e.js runtime 4.32 kB
styles.f34419b46f4c8397f83c.css styles 1.10 kB
main.4b155e348e7d074cc7b0.js main 1.03 kB
Initial Total 57.20 kB

When I upgrade to 11.2.2, this is the initial bundle:

Initial Chunk Files Names Size
main.a68ac17a3ee7b04b27c1.js main 304.40 kB
polyfills.ab27ff871f3f289a1bbd.js polyfills 50.76 kB
runtime.6298cce722c648ea2baf.js runtime 4.14 kB
styles.f34419b46f4c8397f83c.css styles 1.10 kB
Initial Total 360.40 kB

That is a 359Kb size increase of main.js

🔬 Minimal Reproduction

npx @angular/[email protected] new MainBundleIssue

In the new app, update main to:

(async () => {
  const { enableProdMode } = await import('@angular/core');
  const { platformBrowserDynamic } = await import('@angular/platform-browser-dynamic');
  const { AppModule } = await import('./app/app.module');
  const { environment } = await import('./environments/environment');

  if (environment.production) {
    enableProdMode();
  }

  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
})();

then build and notice the main bundle size
after that, ng update @angular/cli @angular/core
Build and notice the increased bundle size.

@alan-agius4
Copy link
Collaborator

alan-agius4 commented Feb 26, 2021

This bootstrapping code was actually never supported. In fact, pre version 11.1, when using @angular/platform-browser-dynamic would cause for it not to be replaced with @angular/platform-browser when using AOT compilation.

The former is used to bootstrap JIT applications.

Before

/*!*********************!*\
  !*** ./src/main.ts ***!
  \*********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
(() => __awaiter(void 0, void 0, void 0, function* () {
    const { enableProdMode } = yield __webpack_require__.e(/*! import() | angular-core */ "default~angular-core~angular-platform-browser-dynamic~app-app-module").then(__webpack_require__.bind(null, /*! @angular/core */ "fXoL"));
    const { platformBrowserDynamic } = yield Promise.all(/*! import() | angular-platform-browser-dynamic */[__webpack_require__.e("default~angular-core~angular-platform-browser-dynamic~app-app-module"), __webpack_require__.e("default~angular-platform-browser-dynamic~app-app-module"), __webpack_require__.e("angular-platform-browser-dynamic")]).then(__webpack_require__.bind(null, /*! @angular/platform-browser-dynamic */ "a3Wg"));
    const { AppModule } = yield Promise.all(/*! import() | app-app-module */[__webpack_require__.e("default~angular-core~angular-platform-browser-dynamic~app-app-module"), __webpack_require__.e("default~angular-platform-browser-dynamic~app-app-module"), __webpack_require__.e("app-app-module")]).then(__webpack_require__.bind(null, /*! ./app/app.module */ "ZAI4"));
    const { environment } = yield __webpack_require__.e(/*! import() | environments-environment */ "environments-environment").then(__webpack_require__.bind(null, /*! ./environments/environment */ "AytR"));
    if (environment.production) {
        enableProdMode();
    }
    platformBrowserDynamic()
        .bootstrapModule(AppModule)
        .catch((err) => console.error(err));
}))();


/***/ })

In 11,.1, with the new Ivy plugin system, a non dynamic import is added and the bootstrapping code is change to use @angular/platform-browser, so in this case the transformer is doing a better job, but has the side-effect that this import is not dynamic.

After

/*!*********************!*\
  !*** ./src/main.ts ***!
  \*********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _angular_platform_browser__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/platform-browser */ "jhN1");

var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};

(() => __awaiter(void 0, void 0, void 0, function* () {
    const { enableProdMode } = yield Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! @angular/core */ "fXoL"));
    const { platformBrowserDynamic } = yield __webpack_require__.e(/*! import() | angular-platform-browser-dynamic */ "angular-platform-browser-dynamic").then(__webpack_require__.bind(null, /*! @angular/platform-browser-dynamic */ "a3Wg"));
    const { AppModule } = yield __webpack_require__.e(/*! import() | app-app-module */ "app-app-module").then(__webpack_require__.bind(null, /*! ./app/app.module */ "ZAI4"));
    const { environment } = yield __webpack_require__.e(/*! import() | environments-environment */ "environments-environment").then(__webpack_require__.bind(null, /*! ./environments/environment */ "AytR"));
    if (environment.production) {
        enableProdMode();
    }
    _angular_platform_browser__WEBPACK_IMPORTED_MODULE_0__["platformBrowser"]()
        .bootstrapModule(AppModule)
        .catch((err) => console.error(err));
}))();

Assuming you are not going to use JIT, to disable the above behaviour you can replace the @angular/platform-browser-dynamic import manually in your code.

(async () => {
  const { enableProdMode } = await import('@angular/core');
- const { platformBrowserDynamic } = await import('@angular/platform-browser-dynamic');
+ const { platformBrowser } = await import('@angular/platform-browser');
  const { AppModule } = await import('./app/app.module');
  const { environment } = await import('./environments/environment');

  if (environment.production) {
    enableProdMode();
  }

-   platformBrowserDynamic()
+   platformBrowser()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
})();

Using dynamic imports, can cause optimization bailouts which will result in larger bundle sizes. in general I don't think that the above is a common good practice to support as users might user the above pattern without fully understanding the consequences.

Seeing that the above pattern is not recommended and there is a workaround for your problem I think it's best not to take action. To support the above pattern, the transformer code will need to be extended to handle this use-case, which few people would benefit from.

@ngbot ngbot bot added this to the needsTriage milestone Feb 26, 2021
@SanderElias
Copy link
Author

@alan-agius4 The provided code is an example of what happens. It's not coding that is used as-is in production code.
The reasons for having a minimal main bundle:

  1. startup performance
  2. make booting angular code optional (especially in SSG this will come in handy)
  3. More control over how the app is bundled,
  4. Start-up light-weight alternatives to angular in SSG

I think those do outweigh the current optimization bailout. Also, optimization bailout is a tooling artifact, and while important, it should not be leading in how to architect an application. When tooling matures, it will start picking up on those patterns.

Thanks for explaining the platform vs platformDynamic, the workaround works, and I don't need JIT in most of my apps anyway.
However, I think there needs to be a broader discussion around this. Perhaps this issue isn't the place for that. Feel free to close this issue when there is a better place to discuss this!

@alan-agius4
Copy link
Collaborator

Following our offline convo.

This is also related to why we use FESM's instead of ESM. #13635 (When not using FESM there would be a size increase of ~5Kb on an ng new app).

While I do see that the benefit for SSG, there are some cavets, the underlying tooling struggles to optimize when having a lot of files (Webpack and Terser). In the above case the initial size of the application more than doubles in size with an increase of about ~240Kb.

Keep tooling in mind when architecting your application is also important IMHO similarly to when you intend to HMR or SSR, if you do want to successfully use such features you need to keep these in mind when architecting your application. Eventually when tooling does support your desired architecture you can update it.

Currently, I don't see as there is anything actionable from our end since this is a limitation of how underlying tools work and expects the JavaScript input. If you do find that such optimization bailout are acceptable for you, there is nothing which is stopping you from using the above bootstrapping code. I do however suggest the above to be used with caution a period analysing of your bundle as overtime you might end up retaining a lot of unused code in your chunks.

Cross chunk optimization is something high on our wishlist as well. Feel free to reach out on Slack to discuss this more.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Mar 29, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants