-
Notifications
You must be signed in to change notification settings - Fork 382
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
perf: apply static parts optimization to dynamic attributes - part 2 #4056
perf: apply static parts optimization to dynamic attributes - part 2 #4056
Conversation
…ntent-optimization-attributes
return (attrs.length > 0 ? ' ' : '') + attrs.join(' ') + (hasClassAttr ? '${2}' : '${3}'); | ||
return attrs.join('') + (hasClassAttr ? '${2}' : '${3}'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified the spacing here so that it's easier to join at runtime, the main issue before was removing the leading space when running on a browser (serialization not required).
attrs.push(`${name}="${htmlEscape(v, true)}"`); | ||
// Inject a placeholder where the staticPartId will go when an expression occurs. | ||
// This is only needed for SSR to inject the expression value during serialization. | ||
attrs.push(hasExpression ? `\${"${v}"}` : ` ${name}="${htmlEscape(v, true)}"`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an expression is encountered, a token is inserted in its place, see the template compiler test fixtures for details.
oldnode: VBaseElement | VStaticPart | null, | ||
node: VBaseElement | VStaticPart, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the browser, attribute patching for static parts is the same as with other elements.
return (partToken: string) => { | ||
// The partTokens are split into 3 section: | ||
// 1. The first character represents the expression type (attribute, class, style, or text). | ||
// 2. The characters from index 1 to the first occurrence of a ':' is the partId. | ||
// 3. Everything after the first ':' represents the attribute name. | ||
// For example: a0:data-name, a = an attribute, 0 = partId, data-name = attribute name. | ||
const type = partToken.charAt(0); | ||
const delimiterIndex = partToken.indexOf(':'); | ||
const partId = partToken.substring(1, delimiterIndex); | ||
const rest = partToken.substring(delimiterIndex + 1); | ||
const part = partIdsToParts.get(partId) ?? EmptyObject; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The format of the tokens will be {type}{partId}:{attributeName}
, for example a0:data-name
.
The token serves as an index to determine which static part value to use for serialization.
The reason this is required is that our current SSR implementation is not compatible with how we are applying attributes in the browser.
The main issue is the vnode.fragment
used in mountStatic
has already been assembled by this routine during the mount process.
In the browser, we rely on cloneNode
to create the DOM elements, loop through those elements and apply the attributes to them.
However, in SSR, the cloneNode
function just returns the string value in vnode.fragment
. This is why we need to insert the expression values at the time the string is being built.
I thought about replicating the cloneNode
behavior to reproduce the nodes in SSR but it would require an HTML parser to generate the nodes.
I feel that this is the most straight forward approach, although I think we should refactor this function to have different implementations between @lwc/engine-dom
and @lwc/engine-server
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great description! We should capture this in a high-level issue and add a TODO here. I think it would be great to have one place where we document this (very big) difference between engine-server
and engine-dom
.
It may ultimately get resolved by the dedicated SSR compiler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a issue! #4078
|
||
if (!isUndefined(parts)) { | ||
// Elements must first be set on the static part to validate against. | ||
traverseAndSetElements(elm, parts, renderer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This step is needed before the validation of the static parts can occur since we compare vnodes against the actual element.
We could alternatively validate while we're traversing down the DOM nodes but I thought this was easier.
for (const attribute of node.attributes) { | ||
const { name, value } = attribute; | ||
if (isExpression(value)) { | ||
this.staticExpressionMap.set(attribute, `a${partId}:${name}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plan to stash all the tokens at this point and they will all follow a similar format:
{type}{partId}:{rest}
a -> attribute
c -> class
s -> style
t -> text
Only attributes will have a value following the :
since we need to identify which attribute to map it to.
@@ -1,25 +1,25 @@ | |||
import { registerTemplate } from "lwc"; | |||
import { parseFragment, registerTemplate } from "lwc"; | |||
const $fragment1 = parseFragment`<a${"a0:href"}${3}>KIX</a>`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of how the tokens will appear.
…ce/lwc into jtu/static-content-optimization-attributes
…ce/lwc into jtu/static-content-optimization-attributes
|
||
const ColonCharCode = 58; | ||
|
||
export function patchAttributes( | ||
oldVnode: VBaseElement | null, | ||
vnode: VBaseElement, | ||
oldnode: VBaseElement | VStaticPart | null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the variable names to node
and oldNode
because VStaticPart technically isn't a vnode as we define it.
Open to suggestions though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oldnode:
VBaseElement | VStaticPart | null,
I changed the variable names to
node
andoldNode
🤨 🤭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First pass review, will do more reviewing later!
} | ||
} | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think an important invariant here is:
- It's never the case that
parts
is undefined on the server but defined on the client (or vice-versa) - It's never the case that
parts
has one length on the server but another on the client
This might be worth a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment!
// Event listeners only need to be applied once when mounting | ||
applyEventListeners(part, renderer); | ||
// Refs must be updated after every render due to refVNodes getting reset before every render | ||
applyRefs(part, owner); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we handle attributes here? Because, if there is no hydration mismatch, then we assume that SSR gave us the correct attributes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, exactly. If there are no hydration mismatches then there shouldn't be a need to patch attributes (because they will be the same).
I'll add a comment explaining it here.
element: Element, | ||
preserveComments: boolean, | ||
codeGen: CodeGen | ||
): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're passing in the whole codeGen
then there's no need to pass in preserveComments
as well. You can get it from the codeGen
.
packages/@lwc/template-compiler/src/codegen/static-element-serializer.ts
Outdated
Show resolved
Hide resolved
|
||
const ColonCharCode = 58; | ||
|
||
export function patchAttributes( | ||
oldVnode: VBaseElement | null, | ||
vnode: VBaseElement, | ||
oldnode: VBaseElement | VStaticPart | null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oldnode:
VBaseElement | VStaticPart | null,
I changed the variable names to
node
andoldNode
🤨 🤭
expect(nodes['deep3nested'].getAttribute('data-dynamic')).toEqual('3'); | ||
expect(nodes['deep4'].getAttribute('data-dynamic')).toEqual('4'); | ||
expect(nodes['deep4nested'].getAttribute('data-dynamic')).toEqual('4'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be interesting to add a test with a variation on this template where everything is in one <div>
. That way, they are all separate part
s of a single static vnode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll make a note and include with the style
and class
attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM. It's a bit disappointing that we have to add so much complexity to handle @lwc/engine-server
, but 1) that should hopefully go away thanks to the SSR compiler, and 2) it's not adding much overhead, thanks to tree-shaking and the minimal "a0:data-foo"
string system you came up with. (A throwaway string is lower overhead than, for instance, a throwaway array.)
I think we should add some additional fixture tests to the template compiler to ensure that the indexes for this string format (a0, a1, etc.) work correctly with code comments and preserve-comments
on vs off. This is something that's bit us in the past: #3933.
I also think this is a good opportunity to do a manual Nucleus before/after comparison before merging, just as a sanity check.
Nice work!
…ce/lwc into jtu/static-content-optimization-attributes
…ce/lwc into jtu/static-content-optimization-attributes
Details
This is part 2 of 2 to apply static parts optimization to dynamic attributes as part of #3624.
This second PR handles the actual changes to our static content optimization to allow attributes with expressions.
The commits in the PR are broken down to more easily review:
Does this pull request introduce a breaking change?
Does this pull request introduce an observable change?
There is an observable change in SSR rendering for MATHML elements that use an attribute as an expression.
When the element contains an attribute expression and has no children it will now render with a self-closing tag. This is the correct behavior and it looks like the previous behavior did not capture it.
EX:
In SSR this will now serialize as:
Previously it would render without the closing tag:
GUS work item
W-15174789