angular / angular-cli

CLI tool for Angular
https://cli.angular.io
MIT License
26.6k stars 11.99k forks source link

Excessive modulepreload Links in Angular 18.0.0-next.4 Affecting Performance Metrics #27490

Open manzonif opened 2 weeks ago

manzonif commented 2 weeks ago

Which @angular/* package(s) are the source of the bug?

platform-server

Is this a regression?

No

Description

I recently upgraded my Angular application to version 18.0.0-next.4 and migrated to esbuild and the new integrated SSR. However, I've noticed a significant impact on performance metrics, particularly First Contentful Paint (FCP) and Largest Contentful Paint (LCP), when using the new version.

Upon further investigation, I found that Angular now splits all JavaScript code into chunks and adds a <link rel="modulepreload"> for each chunk in the <head> of the page. Although the scripts are loaded asynchronously with @defer method, the preloaded scripts seem to be affecting the rendering of critical content, delaying FCP and LCP.

I've conducted tests using Lighthouse, and the results show that removing the modulepreload links improves FCP and LCP metrics. This issue seems to be similar to what was reported in these GitHub discussions:

GoogleChrome/lighthouse#11960 vitejs/vite#5991 In both discussions, users reported improvements in FCP and LCP metrics after disabling or reducing the number of modulepreload links.

Unfortunately, I couldn't find a way to selectively disable modulepreload links in Angular 18.0.0-next.4 based on specific use cases. Therefore, I would like to seek clarification and guidance on how to address this issue to improve performance.

Steps to Reproduce:

Upgrade an Angular application to version 18.0.0-next.4. Enable SSR and use esbuild. Analyze performance metrics using Lighthouse with and without the modulepreload links. Expected Behavior: I expect to see improved FCP and LCP metrics when excessive modulepreload links are removed or selectively disabled.

Additional Information: I have attached screenshots of the Lighthouse test results, showing the impact of modulepreload links on performance metrics.

Screenshots:

Screenshot 1 - Lighthouse Test with modulepreload links

Screenshot 2024-04-17 132612

Screenshot 2 - Lighthouse Test without modulepreload links Performed by removing all modulepreload links server side:

        html = html.replace(
          /<link .?rel="modulepreload" .*?href="(?<href>.+?\.js)".*?>/g,
          ''
        );

Screenshot 2024-04-17 132254

Your assistance in resolving this issue would be greatly appreciated.

Thank you.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 18.0.0-next.2
Node: 20.11.1
Package Manager: yarn 1.22.21
OS: win32 x64

Angular: 18.0.0-next.4
... animations, cdk, common, compiler, compiler-cli, core, forms
... google-maps, language-service, localize, material
... platform-browser, platform-browser-dynamic, platform-server
... router, service-worker

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.1800.0-next.2
@angular-devkit/build-angular     18.0.0-next.2
@angular-devkit/build-optimizer   0.1302.1
@angular-devkit/core              18.0.0-next.2
@angular-devkit/schematics        17.1.2
@angular/cli                      18.0.0-next.2
@angular/ssr                      18.0.0-next.2
@schematics/angular               18.0.0-next.2
rxjs                              7.8.1
typescript                        5.4.5
zone.js                           0.14.4

Anything else?

No response

alan-agius4 commented 2 weeks ago

You can disable to preload tags by using the preloadInitial option.

  "projects": {
    "test": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            "outputPath": "dist/",
            "index": {
              "input": "src/index.html",
              "preloadInitial": false
            },
manzonif commented 2 weeks ago

@alan-agius4 Thank you for your response and for providing the solution.

I would appreciate it if you would consider implementing support for selective filtering of module preloading links. I believe a feature like this would be incredibly valuable, especially for larger applications where fine-grained control over resource loading can have a significant impact on performance.

The implementation described in the Vite.js pull request #9938 seems to provide a robust solution by allowing users to define a resolveDependencies function to filter or modify the list of dependencies. This level of flexibility would empower developers to optimize resource loading based on specific use cases and performance requirements.

Are you planning to incorporate this functionality into Angular in the near future? You might, for example, think about implementing it as part of the @defer block.

alan-agius4 commented 2 weeks ago

@manzonif, I'm curious, how many preload links do you use?

Generally, offering such options doesn't really align with our design goals. Instead, we aim to provide a more responsive default that can cater to applications of all sizes.

There's definitely a correlation between Core Web Vitals (CWV) and preload tags. The more preload tags, the poorer the performance tends to becomes at least based on https://almanac.httparchive.org/en/2021/resource-hints#correlation-with-core-web-vitals

manzonif commented 2 weeks ago

I count about 50 modulepreload links, plus an image that is part of the LCP. Of course it depends on the page being examined.

Furthermore, some of these modules, in turn, require the loading of third-party javascript (with the intention that they should be loaded late),perhaps making the situation even worse.

manzonif commented 2 weeks ago

@alan-agius4, I also noticed that Angular preloaded modules take precedence in the document head over those inserted inside a component. If what @patrickhulce reports here is correct, the order of the preload hints is also important. Therefore, it would be necessary to ensure that the insertion of the module preloads is postponed, at least until after the ngOnInit lifecycle hook of the components.

If I add a preload for an LCP image, it should have high priority, IMO.