-
Notifications
You must be signed in to change notification settings - Fork 27.1k
feat(DomRenderer): allow partial DOM hydration from pre-rendered content #13446
Description
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.
@tbosch & @alxhub please feel free to add anything I missed (or misrepresented).