angular-cli icon indicating copy to clipboard operation
angular-cli copied to clipboard

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

Open manzonif opened this issue 1 year ago • 8 comments

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

manzonif avatar Apr 17 '24 12:04 manzonif

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
            },

alan-agius4 avatar Apr 18 '24 06:04 alan-agius4

@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.

manzonif avatar Apr 18 '24 07:04 manzonif

@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

alan-agius4 avatar Apr 18 '24 07:04 alan-agius4

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 avatar Apr 18 '24 08:04 manzonif

@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.

manzonif avatar Apr 19 '24 03:04 manzonif

Esbuild can dump a stats.json via ng build --stats-json that contains the information which files are required to load lazy loaded chunks. See https://esbuild.github.io/api/#metafile (the outputs part)

Ssr could use the stats.json to only load the chunks required for the current route.

sod avatar May 06 '24 08:05 sod

The most recent RC version contains a change to only generate shallow preload links for initial files. You can find more details in PR https://github.com/angular/angular-cli/pull/27581.

@manzonif, could you please confirm if this resolves your issue?

@sod, in this situation, the issue lies not in the absence of preloads, but rather in having more than approximately 5 preloads, even for files essential to the route. Essentially, including a substantial number of preload links could create a performance bottleneck, more so when using SSR, as it might disrupt the browser's fetch priority.

alan-agius4 avatar May 06 '24 09:05 alan-agius4

Hi @alan-agius4,

Sorry for the delay in replying.

The PR certainly addresses the issue of excessive preload links. However, I have a couple of remaining concerns:

  1. If the order of links is important, I believe that those inserted by the component should take precedence over others. For example, in the case of an LCP (Largest Contentful Paint) image.

  2. Limiting the preloads to the first three chunks might sometimes produce unexpected results. In my case, importing utility functions from other .ts files into the main component results in these small files being the three preload links. While I imagine this issue could be resolved by restructuring the imports, I wanted to bring it to your attention.

Below are some example images:

Screenshot 2024-05-14 114606 Screenshot 2024-05-14 114652

manzonif avatar May 14 '24 10:05 manzonif

I believe that the issue with the order of links has likely been resolved. However, if the problem persists, please provide a reproduction. As for the issue with small chunks, it is currently being tracked here: https://github.com/angular/angular-cli/issues/27715.

alan-agius4 avatar Aug 30 '24 06:08 alan-agius4

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.