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

Route extraction shouldnt load the whole app in dev server #29085

Closed
lennybakkalian opened this issue Dec 9, 2024 · 15 comments · Fixed by #29101
Closed

Route extraction shouldnt load the whole app in dev server #29085

lennybakkalian opened this issue Dec 9, 2024 · 15 comments · Fixed by #29101

Comments

@lennybakkalian
Copy link

lennybakkalian commented Dec 9, 2024

I dont want to be that type of guy that just reopens a issue. But i only saw @alan-agius4 answers on that topic and i would kindly ask other angular engineers about their opinion and if its something that can be solved differently. Maybe without @alan-agius4 indirect implication that it would "only" be solveable with file based routing.

There are no docs on route extraction, but my understanding now is that all routes are computed in advance. I'm not deeper into angular code than the public_api, but I can't imagine that a full application load (full lifecycle or template execution) are necessary to figure this out. What does this have to do with route extraction?

proof of concept
In this example, it would only make sense for SSG if the REQUEST token is not available on the server. (but according to @alan-agius4 its "bad practice" to think that the REQUEST token is available on the server with disabled prerender) - Just think about that for a second.

Sorry if I'm a bit salty, but it took me a really long time to figure out that (imo this bug) is due to the angular dev server.

Related to:
#28995
#28727 (comment)
#29084 (my previous ticket)
Angular Discord - My thread
Angular Discord - Another thread
and probably many more

@lennybakkalian lennybakkalian changed the title Route extraction shouldnt load the whole app twice in dev server Route extraction shouldnt load the whole app in dev server Dec 9, 2024
@alan-agius4
Copy link
Collaborator

In this case, the REQUEST token being null ensures alignment between ng build and ng serve.
To closer mimic the production environment, the application is bootstrapped twice. This approach prevents route extraction from polluting the application state during the rendering of the first route. During the build process, the application is bootstrapped, causing the root component (app.component.ts) to execute and extract routes. At this point, there is no actual request, as highlighted in our documentation: https://angular.dev/guide/hybrid-rendering#accessing-request-and-response-via-di

During route extraction, the application is bootstrapped to:

  1. Retrieve the router configuration and perform validation checks.
  2. Build the routing table to avoid redundant operations during the first request or in serverless/worker environments, where requests can be frequent.
  3. Surface validation errors at build time instead of runtime, ensuring smoother execution.

Bootstrapping enables key operations to be performed ahead of time:

  • Running getStaticParams in the injector context: This ensures dynamic parameters for routing are generated before runtime.
  • Supporting dynamic routes: Routes can be built or modified at runtime, such as when pulling data from an API to construct paths dynamically.
  • Enabling localization of routes: Routes can be adjusted based on user locales, supporting internationalization seamlessly.

By "bad practice," I meant that developers should avoid relying on the request being guaranteed in a server-side application during execution. Ensuring applications are future-proof and adaptable allows them to take advantage of upcoming features where a request might not always be available. This approach also provides flexibility to switch rendering modes in the future if needed.

/cc @dgp1130 for additional input.

@lennybakkalian
Copy link
Author

lennybakkalian commented Dec 9, 2024

Thank you very much for the detailed explanation.

Bootstrapping enables key operations to be performed ahead of time:

Running getStaticParams in the injector context: This ensures dynamic parameters for routing are generated before runtime.
Supporting dynamic routes: Routes can be built or modified at runtime, such as when pulling data from an API to construct paths dynamically.
Enabling localization of routes: Routes can be adjusted based on user locales, supporting internationalization seamlessly.

Aren't these all things for which we don't actually need a component lifecycle? I would have thought that if you create routes dynamically, you would do this via a provider and not via components.

As I said, I am not familiar with the angular internal code, but even for child routers it should somehow be possible to generate these routes AOT without doing a complete component lifecycle. I don't see any reason why components should create routes dynamically, do you? Or have internal things been built so that a refactor would be too big in relation to this bug?

@dgp1130
Copy link
Collaborator

dgp1130 commented Dec 9, 2024

@alan-agius4 is definitely the expert here, so I don't think I have too much more to add. Like he mentioned, route extraction is useful to do at build time even if you don't have any RenderMode.Prerender routes. The serverless point I think is particularly relevant. If we skipped build-time route extraction, serverless deployments need to repeat that work per-request in the worst case, which can be a significant performance regression for large apps in a use case where they likely want peak performance.

Regardless, it feels like you want to write a component which requires a Request object, but I don't think that's currently feasible to do. Angular components still need to support client-side rendering, even if they are only used in pages with SSR because it is always possible to client-side navigate after loading a separate route. So it's generally not safe to require a Request object exists. There's architectural benefits to minimizing this coupling like Alan mentioned, but also doing so would just break CSR anyways (and probably hydration too?).

I think the only time it could hypothetically be safe to require a Request is if you always render a component via @defer with hydrate never and also only have a single route which is always SSR'd. Those are not safe assumptions to make, nor is it a good idea to couple a specific component's implementation to a lack of other routes in your application. Angular components have an implicit requirement that they need to be renderable from scratch on the client. Requiring a Request object breaks that requirement and means certain actions which should work on any Angular component (ex. CSR) now don't work on this particular component.

@alan-agius4
Copy link
Collaborator

Just to add some more, currently when an application is bootstrapped using the bootstrapApplication method in main.server.ts, Angular also bootstraps the root component, which triggers its lifecycle hooks.

Code: https://github.com/angular/angular/blob/46f00f951842dd117653df6cca3bfd5ee5baa0f1/packages/core/src/platform/bootstrap.ts#L136

While this behavior shouldn’t be strictly necessary, it is how Angular currently initializes an application. I’ve been exploring a potential solution to prevent the root component from being invoked during this process. I have a change in mind to do in the framework that should address this issue, but I still need to verify that it doesn’t introduce any unintended side effects or break other functionality.

@lennybakkalian
Copy link
Author

lennybakkalian commented Dec 9, 2024

I have a (probably naive) question without knowing the internal code of angular:

When angular is called with bootstrapApplication, the AppComponent and the appConfig are passed.
All routes are defined as providers in the appConfig. Both static and dynamically generated (via http requests, which you used as an example). Can't the config from provideServerRoutesConfig be used to recognize which components are to be prerendered and only these have the lifecycle? If there is no SSG, it would be unnecessary to call guards, resolver, components and so on, wouldn't it?

I just don't know how to handle this case in my app/library, because requests are made for the first page load, which must return a correct value, otherwise errors will occur. If it's just an app, I could just make a guard that blocks this "ghost" request, but in my case it's a library and you can't do that.

If you are the expert on this topic, of course I respect your decision and you can close the ticket if you really think there is no way around it. I just think it's a bit sad because I really like angular and have liked all the decisions so far, except for this one :/

@alan-agius4
Copy link
Collaborator

hen angular is called with bootstrapApplication, the AppComponent and the appConfig are passed.
All routes are defined as providers in the appConfig. Both static and dynamically generated (via http requests, which you used as an example). Can't the config from provideServerRoutesConfig be used to recognize which components are to be prerendered and only these have the lifecycle? If there is no SSG, it would be unnecessary to call guards, resolver, components and so on, wouldn't it?

We don’t invoke the guards, or lifecycle hooks of all components, only those that are referenced in the AppComponent (root page). I am looking into a way to not do this.

I just don't know how to handle this case in my app/library, because requests are made for the first page load, which must return a correct value, otherwise errors will occur. If it's just an app, I could just make a guard that blocks this "ghost" request, but in my case it's a library and you can't do that.

I am not quite sure of your use-case, but especially in a library, I would say that it paramount not to assume the existing of a request, otherwise the library will not be compatible with other rendering modes including CSR or potentially you will end up using isPlatformServer and isPlatformBrowser quite a bit which isn’t ideal. There is actually a good article about this (written by @CaerusKaru): https://medium.com/@caerus.karu/isplatformbrowser-is-not-your-friend-5ee52b06da89

I am happy to provide more information, if you provide some more context on what you are trying to do in this library.

@lennybakkalian
Copy link
Author

lennybakkalian commented Dec 9, 2024

I am not quite sure of your use-case, but especially in a library, I would say that it paramount not to assume the existing of a request, otherwise the library will not be compatible with other rendering modes including CSR or potentially you will end up using isPlatformServer and isPlatformBrowser quite a bit which isn’t ideal.

My library actually works exactly like the http client (with hydration and csr). I would have the same problem with the http client.

Imagine an empty angular app where an auth guard http.get('/api/user/self') is used in the root path. If a cookie with the user token exists in the header, the current user is returned, otherwise a new one is created and returned. The problem is that no header (no REQUEST) is found in ssr context by this “ghost” bootstrapApplication and therefore a new user is created. I think this "ghost" call should not happen for routes that have RenderMode.Server

In prod it wouldn't be a problem, I understand that. But during development, a new user is created with every frontend change. This is just an example and I am aware that this behavior can be avoided with a dirty fix. if(isServer && !REQUEST){ return false } but i don't think it's such an elegant solution tbh

I strongly believe that the more people upgrade now and use ssr in the dev server, they will have this same issue.

@alan-agius4
Copy link
Collaborator

If a cookie with the user token exists in the header, the current user is returned, otherwise a new one is created and returned.

However, request.cookie is not available in CSR. How is this currently handled in CSR when users navigate between pages, and no server request is made or the cookie is absent?

The problem is that no header (no REQUEST) is found in ssr context by this “ghost” bootstrapApplication and therefore a new user is created

I'm not fully clear on the "create user" process. Are you creating a new user for every request that lacks a cookie? For instance, if I make 10,000 requests without a cookie, will 10,000 new users be created? This seems like a significant security risk unless your application is internal and not publicly accessible.

if(isServer && !REQUEST){ return false } but i don't think it's such an elegant solution tbh

I agree, relying on isBrowser or isServer checks isn't great. When handling different logic for server and client in general you want services to handle difference in logic, Ex:

// auth.service.ts
@Injectable()
export abstract class AuthService {
  abstract isAuthenticated(): Promise<boolean>;
  getUserId() { }
  createUser() { }
  // other common methods
}

// browser-auth.service.ts (Browser Implementation)
export class BrowserAuthService extends AuthService {
  isAuthenticated(): boolean {
    const token = document.cookie;
    return !!token; 
  }
}

// server-auth.service.ts (Server Implementation)
export class ServerAuthService extends AuthService {
  private readonly request: Response | null = inject(REQUEST);

  isAuthenticated(): Observable<boolean> {
    const token = this.request?.headers?.get('cookie');
    return !!token; 
  }
}

// auth.guard.ts
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  const isAuthenticated = authService.isAuthenticated();
};

@lennybakkalian
Copy link
Author

lennybakkalian commented Dec 10, 2024

This seems like a significant security risk unless your application is internal and not publicly accessible.

Something like Anonymous Sign-Ins like supabase does it are totally fine and are no security risk.

TLDR:
Anonymous sign-ins can be used to build:
E-commerce applications, such as shopping carts before check-out
Full-feature demos without collecting personal information
Temporary or throw-away accounts

But it's not about the actual implementation. I just wanted to make a simple example to demonstrate what side effects a “ghost” bootstrap would have.

Because of this unexpected behavior, there will be more and similar issues/questions like this:
https://stackoverflow.com/questions/79158723/angular-cookie-not-available-in-the-first-execution-of-app-initializer-function
Someone there said: "So you can ignore these bugs and carry on development, because when you deploy with SSR, it should work fine." And where he is right, I find such a proposed solution for a framework that should work in a comprehensible way somewhat painful.

@alan-agius4
Copy link
Collaborator

Thank you for the additional information. I am currently focusing on resolving the issue, rather than running the AppComponent.

To clarify and correctness, while the issue described in https://stackoverflow.com/questions/79158723/angular-cookie-not-available-in-the-first-execution-of-app-initializer-function may seem similar, it is actually a very different problem. The root cause there is that the APIs used are tightly coupled with Express, which makes them non-portable and incompatible with the dev-server.

alan-agius4 added a commit to alan-agius4/angular that referenced this issue Dec 10, 2024
Introduced the `DISABLE_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `true`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `false`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Dec 10, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes angular#29085
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Dec 10, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes angular#29085
@lennybakkalian
Copy link
Author

So you could theoretically disable component bootstrapping for route extraction in the dev server, but enable it again later during the actual build? That would be a really nice change

@alan-agius4
Copy link
Collaborator

So you could theoretically disable component bootstrapping for route extraction in the dev server, but enable it again later during the actual build? That would be a really nice change

Yes

alan-agius4 added a commit to alan-agius4/angular that referenced this issue Dec 10, 2024
Introduced the `DISABLE_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `true`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `false`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085
alan-agius4 added a commit to alan-agius4/angular that referenced this issue Dec 10, 2024
Introduced the `ENABLE_ROOT_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `false`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `true`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Dec 11, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes angular#29085
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Dec 11, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes angular#29085
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Dec 11, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes angular#29085
alan-agius4 added a commit that referenced this issue Dec 11, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes #29085
alan-agius4 added a commit that referenced this issue Dec 11, 2024
…ction

This commit disables component bootstrapping during route extraction to prevent invoking the AppComponent and its lifecycle hooks.

Closes #29085

(cherry picked from commit 10a5b8b)
@stewieoO
Copy link

stewieoO commented Dec 12, 2024

@alan-agius4 Just to make sure, the route guards are included in this right? Cause i'm currently facing the issue with a canActivateFn that is making an api call.

Edit: I just saw it's already included in 19.0.5. For some reason it's not working for me. I still get calls to the root component during build time (with an empty REQUEST token).
I tried to reproduce it with an empty project but can't get it to "break". Would you have any idea what could cause this before i'm having to start commenting out everything one by one? :D

@MarcoGlauser
Copy link

As an additional data point.
This change also fixed our timeout during routes extraction errors we were facing in our ci environment, since the api endpoints aren't available during build time :)

alan-agius4 added a commit to alan-agius4/angular that referenced this issue Dec 16, 2024
Introduced the `ENABLE_ROOT_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `false`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `true`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085
alan-agius4 added a commit to alan-agius4/angular that referenced this issue Dec 16, 2024
Introduced the `ENABLE_ROOT_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `false`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `true`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085
alan-agius4 added a commit to alan-agius4/angular that referenced this issue Dec 16, 2024
Introduced the `ENABLE_ROOT_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `false`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `true`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085
AndrewKushnir pushed a commit to angular/angular that referenced this issue Dec 16, 2024
Introduced the `ENABLE_ROOT_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `false`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `true`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085

PR Close #59133
AndrewKushnir pushed a commit to angular/angular that referenced this issue Dec 16, 2024
Introduced the `ENABLE_ROOT_COMPONENT_BOOTSTRAP` token to control the bootstrapping of components during application initialization. This token is utilized by the Angular CLI in the `@angular/ssr` package, particularly during server-side rendering (SSR) when extracting routes.

When set to `false`, this token prevents the root component from being bootstrapped during SSR's route extraction phase, which is crucial for efficiently extracting routes without triggering component initialization. This mechanism separates the concerns of route extraction and component bootstrapping during SSR rendering, optimizing performance.

If not provided or set to `true`, the default behavior of bootstrapping the root component(s) during initialization is maintained.

Context: angular/angular-cli#29085

PR Close #59205
@jimjim2a
Copy link

@alan-agius4 Sorry If I'm not in the good issue or It the topic has been addressed somewhere else (I haven't found it), I'm not clear about dynamic route paths (fetched from an API for example).

Let's say my app has no static routes, but only routes paths based on an API. During APP_INITIALIZER the router config is updated using router.resetConfig method.

In this case, since all routes paths are built dynamically, in the server.config I have this :

provideServerRoutesConfig(
    [{ path: '**', renderMode: RenderMode.Server }]
)

While provideRouter looks like this

provideRouter([])

If I understand well, during the build, the app bootstrap is designed to extract the router config before the first SSR request.

So my question is, if a route path has changed, or another path is added after the app build, what is supposed to happen ?

Ex :

{ path: 'slugA', component: ComponentA }

is now

{ path: 'new-slugA', component: ComponentA }

What happens if I reach /new-slugA ?

Thank a lot for your explanation !

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

Successfully merging a pull request may close this issue.

6 participants