-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(core): add hydration protected elements
With this commit, we are now able to handle hydration-protected elements and their attributes. This enhancement is aimed at improving UX and reducing the number of HTTP requests made by browsers to external resources during the hydration process. Take, for example, the `iframe` element with a static `src` attribute, which is rendered on the server. When we begin hydrating that element on the client, it attempts to set up the static attributes again. This includes setting the `src` attribute again with the same value it already has. Browsers interpret each change to the `src` attribute as a new request to load the specified resource. Consequently, the video may flicker as the browser attempts to reload it. We now maintain a map of elements that should be protected from hydration. Each element (acting as a key) maps to a list of attributes, for instance, `iframe -> ['src']`. When we look up an existing element, we check its tag name and if there's a list of attributes associated with it. If we encounter an `iframe` (or other protected element), we iterate over its protected attributes and check whether that element already has those attributes set. If it does, we save this information in the hydration info map to use it when calling `setAttributes`.
- Loading branch information
Showing
21 changed files
with
343 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
packages/core/src/render3/util/attrs_utils_with_hydration_support.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {DehydratedView} from '../../hydration/interfaces'; | ||
import {Renderer} from '../interfaces/renderer'; | ||
import {RElement} from '../interfaces/renderer_dom'; | ||
|
||
/** | ||
* This represents a mapping of element tag name to an attribute name that should not be reset | ||
* during hydration in case the value is the same. | ||
* Values are implemented as a list in case they need to be expanded in the future to accommodate | ||
* more cases. | ||
*/ | ||
const hydrationProtectedElementToAttributeMap = new Map<string, string>([ | ||
// All these elements and their attributes force the browser to reload resources when they're set | ||
// again with the same value. For example, consider an `<object>` element with its `data` | ||
// attribute set to `/assets/some-file.pdf`. When the browser retrieves an HTML document from the | ||
// server and finishes parsing it (after the document state is set to `complete`), it loads | ||
// external resources such as images, videos, audios, etc. This includes loading | ||
// `/assets/some-file.pdf`. Subsequently, when Angular begins its hydration process, it attempts | ||
// to call `setAttribute` on the `<object>` element again with `setAttribute('data', | ||
// '/assets/some-file.pdf')`. This action forces the browser to reload the same resources, even | ||
// though they have already been loaded previously. | ||
['iframe', 'src'], | ||
['embed', 'src'], | ||
['object', 'data'], | ||
]); | ||
|
||
export function getHydrationProtectedAttribute(tagName: string): string | undefined { | ||
return hydrationProtectedElementToAttributeMap.get( | ||
// Convert to lowercase so we cover both cases when the tag name is `iframe` or `IFRAME`. | ||
tagName.toLowerCase(), | ||
); | ||
} | ||
|
||
let _setAttributeWithHydrationSupport: typeof _setAttributeWithHydrationSupportImpl = ( | ||
hydrationInfo: DehydratedView | null, | ||
renderer: Renderer, | ||
nodeIndex: number, | ||
element: RElement, | ||
attributeName: string, | ||
attributeValue: string, | ||
namespace?: string | null, | ||
) => { | ||
renderer.setAttribute(element, attributeName, attributeValue, namespace); | ||
}; | ||
|
||
function _setAttributeWithHydrationSupportImpl( | ||
hydrationInfo: DehydratedView | null, | ||
renderer: Renderer, | ||
nodeIndex: number, | ||
element: RElement, | ||
attributeName: string, | ||
attributeValue: string, | ||
namespace?: string | null, | ||
) { | ||
if ( | ||
getHydrationProtectedAttribute(element.tagName) && | ||
hydrationInfo?.protectedAttributes?.has(nodeIndex) | ||
) { | ||
// If the index of the currently hydrating element is found in the `protectedAttributes` map, it | ||
// indicates that this element has hydration-protected attributes (for example, if the `element` | ||
// is an `iframe` with its `src` attribute set). | ||
const protectedAttributeValue = hydrationInfo.protectedAttributes.get(nodeIndex)!; | ||
if (attributeValue === protectedAttributeValue) { | ||
// An initial value is protected for the first `setAttribute` call, further protection would | ||
// happen as a part of the change detection mechanisms. Remove the node index entry | ||
// from the map to indicate that no special logic are required for this element and a given | ||
// attribute. | ||
hydrationInfo.protectedAttributes.delete(nodeIndex); | ||
// We do not want to reset the value since it may negatively affect performance and UX. For | ||
// example, in the `<iframe src="...">` case, this would cause a reload within the iframe. | ||
return; | ||
} | ||
} | ||
|
||
renderer.setAttribute(element, attributeName, attributeValue, namespace); | ||
} | ||
|
||
export function _setAttributeImpl( | ||
hydrationInfo: DehydratedView | null, | ||
renderer: Renderer, | ||
nodeIndex: number, | ||
element: RElement, | ||
attributeName: string, | ||
attributeValue: string, | ||
namespace?: string | null, | ||
) { | ||
_setAttributeWithHydrationSupport( | ||
hydrationInfo, | ||
renderer, | ||
nodeIndex, | ||
element, | ||
attributeName, | ||
attributeValue, | ||
namespace, | ||
); | ||
} | ||
|
||
export function enableSetAttributeWithHydrationSupportImpl() { | ||
_setAttributeWithHydrationSupport = _setAttributeWithHydrationSupportImpl; | ||
} |
Oops, something went wrong.