Sasha
Sasha

Reputation: 51

Unused Injectable services with providedIn: 'root' are Included in main.js, increasing bundle size (Angular 19, Standalone)

Issue:

In an Angular 19 standalone application, I have an API client file containing over 100 @Injectable services, each marked with providedIn: 'root'. However, only 3–4 of these services are used in the app. The problem is that all unused services are still included in the production build (main.js), significantly increasing the bundle size.

Details:

I’m building a new Angular app using the full standalone approach (Angular 19). I also have another app built with Angular 17 that follows the traditional NgModule-based approach. Both apps use identical API clients generated via NSwag Studio, which creates multiple @Injectable services like this:

@Injectable({
    providedIn: 'root'
})
export class FeatureClient {}

In the modular app (Angular 17): Unused services are excluded from the final bundle (tree-shaking works as expected). Used services are included in main.js only when explicitly injected.

In the standalone app (Angular 19): All services from the API client are included in main.js, regardless of whether they are used or not.

Manually removing any unused code from the API client obviously decreases the bundle size. To further verify that's the case for both apps I did the following:

  1. Add a mock service to the API client file.
  2. Build the application for production.
  3. Check if the mock service is present in the final bundle (main.js).
  4. Inject the mock service in a component, rebuild, and verify the output again.

Results:

Modular app: f the service is not used, it is excluded from the bundle. If the service is injected, it is included in main.js.

Standalone app: included in both scenarios regardless of the injection.

Upvotes: 2

Views: 93

Answers (2)

Sasha
Sasha

Reputation: 51

Cause: the issue is caused by "isolatedModules": true flag in tsconfig.json.

Explanation: when isolateModules is set to true, it applies some typescript restrictions to ensure that each file can be independently transpiled. This flag prevents typescript from doing the full code analysis needed to generate metadata for tree-shaking and causes all @Injectable() services to be included in the result bundle. It's because the compiler cannot tell whether the service is used or not.

Fix: remove isolateModules from tsconfig.json or set it to false.

Upvotes: 1

Naren Murali
Naren Murali

Reputation: 57986

You can perform this checklist to eliminate certain scenarios that might cause this:

  1. The standalone components you created (which use these services), should be lazy loaded using loadComponent -> Lazy Load Standalone Components with "loadComponent". Eagerly loaded components will be bundled into the main.js.

  2. If the components (which use these services) are loaded directly in the HTML, they will cause the services to be included. You can leverage @defer to ensure, they are loaded only when they are visible in the viewport. There are tons of other loading options, refer: Deferred loading with @defer

     @defer (on viewport) {
       <large-cmp />
     } @placeholder {
       <div>Large component placeholder</div>
     }
    
  3. The service, should be not be used in the initial default page, for example: if your homepage uses 5 services. These services will be loaded since they are dependencies of the home page.


If you believe the above suggestions do not apply to you, you should create a minimal reproducible stackblitz and raise a bug on Github Angular - Issues, so that the Angular team can look into this. The more details and working example provided, will greatly help the team, find the issue.

Upvotes: 0

Related Questions