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

feat(DomRenderer): allow partial DOM hydration from pre-rendered content #13446

Closed
jeffbcross opened this issue Dec 14, 2016 · 189 comments
Closed
Labels
area: server Issues related to server-side rendering feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature state: Needs Design
Milestone

Comments

@jeffbcross
Copy link
Contributor

I'm recapping a discussion I just had with @alxhub and @tbosch. This is mostly Tobias' design.

I'm submitting a ... (check one with "x")

[x] feature request

Current behavior

Typically when a page is pre-rendered, such as with Universal, the Angular application bootstraps in the browser, then blows away the pre-rendered content, and replaces it with newly created nodes. The DOM tree that is pre-rendered is often very similar, or identical to the DOM tree created on the front end, but there are issues that make it difficult for Angular to take over the existing DOM tree (otherwise referred to as DOM hydration).

The actual handoff of destroying the old DOM and showing the new DOM is pretty fast and seamless, so it's not necessarily a major UX issue in and of itself. Where it becomes problematic is in cases like ads that load in iframes (which is pretty much all display ads). If these ad iframes are pre-rendered -- which is a business requirement for many publishers -- and the iframe gets moved in the DOM, the iframe will refresh. This causes some ad networks to suspect abuse, as if publishers are trying to sneak more ad views.

Why Not Use the Already-Rendered DOM?

One issue is that with asynchronicity+conditional DOM (i.e. *ngIf="data | async"), the tree in the client may be rendered before the condition is truthy, whereas the pre-rendered version may have the full tree with async data resolved.

Another challenge is that text nodes are not able to be selected by CSS selectors, which would mean the renderer would have to rely on child ordering in order to associate pre-rendered nodes with client-rendered nodes (which is not always correct). Similar challenge goes for elements in an *ngFor, the order must be assumed to be identical.

The renderer would also be responsible for cleaning up pre-rendered orphan nodes. i.e. if 30 items in an *ngFor were pre-rendered, but only 20 were rendered in the client, the additional 10 nodes would need to be removed to prevent unexpected behavior.

Proposal: Optional, explicit, partial DOM Hydration

Allow setting a user-specified attribute on elements to associate the pre-rendered version with client-rendered version. If the renderer comes to a node that it can't associate with an existing node, it will blow away the node and re-create it. The developer would be responsible for setting these ids on the elements they care about. Example:

import { HydrateDomConfig, NgModule } from '@angular/core';

@NgModule({
  providers: [
    { 
      provide: HydrateDomConfig, 
      useValue: {
        hydrate: true, // default false for backwards compat
        attribute: 'pid', // default 'id'
      } 
    }
  ]
})

Component:

@Component({
  template: `
    <div pid="header">
      <header-ad pid="header-ad"></header-ad>
      <div>
        <!-- this will get blown away and re-created since it lacks attribute -->
      </div>
    </div>
  `
})

This design allows the DomRenderer to traverse the DOM tree and match elements for each branch starting at the root until it can't go any deeper, at which point it would blow away the descendants and re-create them.

Text nodes would all be destroyed and re-created with this design, as well as any node that doesn't have the set attribute, pid.

I don't expect that the rendering would be user-perceivable, other than if there are discrepancies between pre-rendered and client-rendered DOM, but that's a concern even without this feature.

CC @gdi2290 @pxwise

@tbosch & @alxhub please feel free to add anything I missed (or misrepresented).

@vicb vicb added area: core Issues related to the framework runtime state: Needs Design feature Issue that requests a new feature labels Dec 14, 2016
@pxwise
Copy link

pxwise commented Dec 14, 2016

Ad wipeout on client bootstrap is a real world issue for us using universal and this hydration proposal should solve it. Our current workaround gets us partway there, moving server DOM into the same position in client rendered DOM but does not outsmart ad verification services that watch for DOM mutations, hiding the ad upon client bootstrap.

Big thumbs up for opt-in hydration.

@PatrickJS
Copy link
Member

PatrickJS commented Dec 14, 2016

LGTM, Keep in mind this is a huge problem with all js frameworks so for Angular to have a solution is a great win for when comparing different SSR solutions. The real solution is having Ads actually work together with js frameworks but that will never happen, other than AMP. I can see how it would be implemented by rewriting selecting root element and providing a different render path.

For the |async you definitely have that problem with Promises due to the microtask while Observables do return synchronously. On the client, we can assume there will be batched data sent from the server to the client which gives us all of the results immediately. For Universal, we need to have the data available to the client synchronously anyways to reuse the server Http responses

var res = {data: 'ok'};
var api = Rx.Observable.of(res);
var api2 = Promise.resolve(res);

var vm = {};
var vm2 = {};
api.subscribe(function(data) { vm = data; });
api2.then(function(data) { vm2 = data; });
console.log(vm); // {data: 'ok'}
console.log(vm2); // {}

@jeffbcross
Copy link
Contributor Author

An alternative design would be to provide an alternate renderer that would extend DomRenderer, rather than modifying DomRenderer to behave differently depending on the hydrate value.

@NgModule({
  providers: [{
    provide: Renderer, useClass: DomHydrationRenderer
  }]
})

@PatrickJS
Copy link
Member

PatrickJS commented Dec 15, 2016

+1 for DomHydrationRenderer

@IgorMinar
Copy link
Contributor

Two things:

  • angular.io v42 might also benefit from this. I had to put in a bunch of code that prevented some flickering caused by the content being wiped out by the renderer code when initializing components
  • I wonder if there is something from the AMP solution that we could leverage to in universal, it would be good to look into that before we go too creative with our own solution/workaround.

@playground
Copy link

+1 either solution will be beneficial and vital for us to move forward.

@tbosch
Copy link
Contributor

tbosch commented Dec 28, 2016

We can still make the work generically, when using auto generated ids based on the place in the element hierarchy (and not based on the creation order).

I think we should think this through as well before we decide.

@FahadMullaji
Copy link

is HydrateDomConfig part of @angular/core? I get error when I try to compile the application.

error TS2305: Module '"/home/fahad/Workspace/siteCuriouss/node_modules/@angular/core/index"' has no exported member 'HydrateDomConfig'.

@DzmitryShylovich
Copy link
Contributor

@FahadMullaji it's only a proposal.

@josephliccini
Copy link

This proposal is super exciting!

Has any thought been given to pre-rendered lazy routes? We can achieve pre rendered lazy routes via https://github.com/angular/universal/blob/master/modules/ng-module-map-ngfactory-loader/README.md

Is it possible for the hydration to occur after lazy route is fetched and then rendering begins?

Just curious on thoughts here.

Universal has been great and straightforward to use so far; thanks to everyone involved!

@mcferren
Copy link

What couples the ComponentRef obj to the Dom string (cmpref.changeDetectorRef.rootNodes[0] & cmpref.hostView.rootNodes[0] ect)? Is there an explicit reference(i.e. dom string selector) or does this binding live in memory? Is there a possibility of calling createComponent with an existing dom node as an argument?

@vytautas-pranskunas-
Copy link

Any updates on this - i have big issue with this as everybody here :(

@FahadMullaji
Copy link

FahadMullaji commented Aug 20, 2018 via email

@vytautas-pranskunas-
Copy link

vytautas-pranskunas- commented Aug 20, 2018 via email

@PatrickJS
Copy link
Member

PatrickJS commented Aug 20, 2018

@vytautas-pranskunas-
Every framework, other than angular, has solved this problem. With that said it might not be a problem once the new ivy renderer is released.

@vytautas-pranskunas-
Copy link

vytautas-pranskunas- commented Aug 21, 2018 via email

@naveedahmed1
Copy link
Contributor

For the ads, I think the ad code shouldn't initialize on sever, if it does I think it would be a policy issue for most of the ad networks/advertisers including Google Adsense.

For this we can check and include ad code only when in browser.

You can check if its running in browser

import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

private isBrowser = isPlatformBrowser(this.platformId);

constructor(@Inject(PLATFORM_ID) private platformId) { }

Adding ad code to SPA is little tricky, if your page is making an ajax request and main content of the page depends on completion of that request you should also wait for that request to complete before you add ad code to the page. Otherwise, if ad code is initialized before ajax request completes (and contents are populated) it would also cause ad network policy issue.

@PatrickJS
Copy link
Member

yeah the issue isn't rendering ads on the server so much as trying to make sure angular doesn't remove/edit/change the ssr element. There was a solution which reattach the ad dom from the server to the new dom created by the client. Again the problem with that is the ad detecting dom manipulation. So the only reason why people want hydration is for ads since it's always faster just to replace the ssr view with csr view. There were solutions made to also keep the element in the dom and insert/remove around that element but would require some rewriting of the renderer to keep track of the elements.

Ivy render will likely solve this since you can choose which elements you want to boot angular into and ignore the ad elements.

@vytautas-pranskunas-
Copy link

vytautas-pranskunas- commented Aug 22, 2018

Adds are not only reason why people want hydration. Hydration needed for prerendering because in scenario when page content is fetched after bootstrapping SSR has no flashing but prerendered. So hydrations needed for prerendered pages also and not sure if ignoring will solve this problem or you will have to ignore whole body because content is everywhere.

I think best solution would be something simmilar like React Virtual DOM which checks what parts where changed and update only those one.

@BlindDespair
Copy link

Hello everyone,

As Minko mentioned above, we were focusing on delivering the necessary pieces of functionality to support full application non-destructive hydration. This feature allows Angular to reuse existing DOM structures on the client that were rendered by the server without having to destroy and re-render everything from scratch.

We wanted to share that the full application non-destructive hydration would be available in the developer preview mode in the upcoming v16 release of Angular! You can try it out today by updating to the pre-release version (using the --next flag) and enabling the feature as described in the guide. If you come across any issues or have any feedback, please let us know by creating a new ticket using the issue tracker.

You can also find additional information about the feature and our future plans in the blog post that was published today.

Thank you.

Good stuff. Although the guide says the app must use SSR and points to a Universal guide. Does it mean it's not gonna work with SSG solutions like Scully?

@AndrewKushnir
Copy link
Contributor

Good stuff. Although the guide says the app must use SSR and points to a Universal guide. Does it mean it's not gonna work with SSG solutions like Scully?

@BlindDespair hydration can be enabled in other systems that uses Angular APIs such as renderModule or renderApplication for server-side rendering. However each system may require some extra configuration (and extra code) to enable the feature, depending on a specific implementation of the system. // cc @SanderElias

@BlindDespair
Copy link

Good stuff. Although the guide says the app must use SSR and points to a Universal guide. Does it mean it's not gonna work with SSG solutions like Scully?

@BlindDespair hydration can be enabled in other systems that uses Angular APIs such as renderModule or renderApplication for server-side rendering. However each system may require some extra configuration (and extra code) to enable the feature, depending on a specific implementation of the system. // cc @SanderElias

By default, Scully uses Puppeteer to prerender the app meaning there's no server involved. It just takes a snapshot of a page when it becomes idle. The DOM is rendered by Angular in this case, but naturally none of the APIs you mention get called, so hydration is not gonna work. Scully does have the option to prerender using platform server, but the puppeteer way makes development much more simple. Is there a chance to make it possible to generate the ngh data in the browser, serialize it and then provide it separately when pre-rendered app starts? Just curious if it's a dead end for snapshot-based pre-rendering or if there's still hope.

@SanderElias
Copy link
Contributor

@AndrewKushnir When we are using the PlatfromServerRenderer, it does work, but it comes with the known issues that are there for Angular Universal.
I would love to have a way for Scully to enable the needed things when rendering in a browser. Our primary renderer uses Puppeteer, and we also support rendering using PlayWright. From what I have been seeing during testing, things needed for hydration are missing there, so the dom still gets destroyed once the app boots.

Would it be possible to support that?
cc @jessicajaniuk

@jessicajaniuk
Copy link
Contributor

@SanderElias The hydration logic is integrated with platform-server and requires it to provide the necessary annotation functionality during application serialization. Puppeteer is essentially no different than client side rendering from the perspective of the framework since puppeteer is just a headless chrome instance. Hydration is not supported outside of server rendering environments that depend on platform-server.

@SanderElias
Copy link
Contributor

@jessicajaniuk Would it be possible to get what is needed there? If I know what to add during serialization, I can do this inside our renderer(s). Is this serialization format going to be documented?

@jasoche-n
Copy link

Hello everyone,

As Minko mentioned above, we were focusing on delivering the necessary pieces of functionality to support full application non-destructive hydration. This feature allows Angular to reuse existing DOM structures on the client that were rendered by the server without having to destroy and re-render everything from scratch.

We wanted to share that the full application non-destructive hydration would be available in the developer preview mode in the upcoming v16 release of Angular! You can try it out today by updating to the pre-release version (using the --next flag) and enabling the feature as described in the guide. If you come across any issues or have any feedback, please let us know by creating a new ticket using the issue tracker.

You can also find additional information about the feature and our future plans in the blog post that was published today.

Thank you.

Hey, this is great news. I'm wondering is this the reason for dropping support for preboot? If so, I can see how this would remove most of the functionality preboot provided, but how is a form submission attempt handled prior to bootstrapping on the client side? Will this just be something provided by the ServerModule?

If there's been previous discussion on this then please point me to it. I haven't been able to find much information around dropping support for preboot. Thanks.

@retry2z
Copy link

retry2z commented May 3, 2023

I have played with the new feature and I see a few missing parts.

  • We can't prevent/skip component from recreating it's template - this was the main goal here, right?
  • We can't apply ngSkipHydration to ng-container

I have a page with dynamic components only and the whole page is recreating. In this case even the ngSkipHydration directive doesn't help

@component({
selector: 'section[lazy]',
template: "
<ng-container *ngIf="component$ | async as component">
<ng-container *ngComponentOutlet="component.template; injector: component.injector">

"
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule]
})

@JeanMeche
Copy link
Member

@retry2z Hi thanks for your feedback, you should open a new issue on this to give it more visibility !

@AndrewKushnir
Copy link
Contributor

@retry2z thanks for the feedback.

We can't prevent/skip component from recreating it's template - this was the main goal here, right?

Could you please provide some additional information on what the expected vs actual behavior is? As @JeanMeche mentioned, it'd be great if you could create a new ticket about it in the issue tracker (with a minimal repro), so that we can take a look and discuss further.

We can't apply ngSkipHydration to ng-container

This behavior is intentional: the ngSkipHydration can be applied only to a hydration boundary, which is a component. See additional information about it in the hydration guide. I think that in your case you can apply the ngSkipHydration to the component (with the section[lazy] selector) as a host attribute instead.

@retry2z
Copy link

retry2z commented May 3, 2023

The yeah didn't provide the needed context what is the expected behavior here, so in a few days will try to provide repo.

It seems to recreate the components, if they contains ng-container with componentOutlet it doesn't skips even if we apply the directive to the host. In the console count the component as skipped, but I can see the recreation of the template clearly.

First thing first, will try to recreate it with a new project with clear guide, because this effect is from a pre update tests of a project.

@usrrname
Copy link

usrrname commented Aug 26, 2023

@SABRYX possibly related to FAST-SSR: #13446 (comment)

@princ09

This comment was marked as off-topic.

@naveedahmed1

This comment was marked as off-topic.

@PatrickJS
Copy link
Member

can we close this issue. there's no reason to keep this open

@princ09

This comment was marked as off-topic.

@princ09

This comment was marked as off-topic.

@denisyilmaz

This comment was marked as off-topic.

@princ09

This comment was marked as off-topic.

@denisyilmaz

This comment was marked as off-topic.

@bierzorutas

This comment was marked as off-topic.

@princ09
Copy link

princ09 commented Feb 17, 2024

Thanks @bierzorutas and @denisyilmaz for your input.
@denisyilmaz that font flickering is happening because of font-display : swap property. That can be another issue with hydration.
I would love to hear from contributor and developer of Angular, please have look into this code. Let me know what I am missing here.

@alxhub
Copy link
Member

alxhub commented Mar 30, 2024

Closing as resolved :)

Note that the original issue is not describing partial hydration, but a workaround for the issues with the destructive "hydration" that Angular had originally, to allow partial reuse of some of the DOM during the client-side bootstrapping.

Partial hydration is indeed on our roadmap, but that's not what this issue tracks :)

@alxhub alxhub closed this as completed Mar 30, 2024
Feature Requests automation moved this from Proposed Projects to Closed Mar 30, 2024
@PatrickJS
Copy link
Member

yes, the future merging with Wiz is amazing.

Everything I wanted for Angular Universal is finally happening and it's exciting to see 🎉

There are more things, I'll share in the future, that are only possible with Resuming frameworks and Signals so I'm glad Angular can be part of that 🙌

I want to thank everyone who believed in me with Angular Universal and the vision I had for it.
Angular has a bright future ahead 🥂

image

@JeanMeche
Copy link
Member

image

@naveedahmed1
Copy link
Contributor

Wow, what a historic moment! Farewell, dear issue! 🎉 It feels like saying goodbye to an old friend we've been following since 2016. The bond we've formed over the years, the ups and downs, the anticipation... it's been quite the journey! 😄 Though I'll miss our regular updates, I'm excited for the next chapter in Angular's evolution. Cheers to the closure and onwards to partial hydration! 🚀 #Goodbye2016Issue #HelloPartialHydration

@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 Apr 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: server Issues related to server-side rendering feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature state: Needs Design
Projects
No open projects
Development

No branches or pull requests