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

perf(engine-core): reduce fragment cache objects #4431

Merged
merged 10 commits into from
Aug 6, 2024

Conversation

nolanlawson
Copy link
Collaborator

Details

This is a small perf improvement to how we handle fragment caches. The main benefit is in using less memory and creating fewer objects.

Fragment caches are used in compiled templates. They're used whenever you see e.g.:

const $fragment1 = parseFragment`<div ${3}>yolo</div>`;

Under the hood, parseFragment will cache based on 1) the tagged template literal and 2) a cache key based on synthetic-vs-native shadow and whether scoped styles are present. The reason for the second cache key is that the HTML string will be different in different cases:

<!-- no styles-->
<div>yolo</div>

<!-- synthetic shadow with styles -->
<div lwc-123abc>yolo</div>

<!-- scoped styles -->
<div class="lwc-123abc">yolo</div>

<!-- synthetic shadow with scoped styles -->
<div lwc-123abc class="lwc-123abc">yolo</div>

(The main reasons for this are 1) historical – synthetic shadow scoping uses attributes whereas scoped styles use classes, and 2) performance – we avoid rendering attributes/classes unless absolutely necessary.)

The problem with the current fragment cache is that it creates one cache object per tagged template literal. Each cache object is just a mapping from number to Element. This is wasteful because there are only 4 numbers we have to worry about (corresponding to the 4 cases above). So we only really need 4 maps. I.e.:

// wasteful!
taggedTemplate -> number -> Element

// better!
number -> taggedTemplate -> Element

Benchmark

This PR is perf-neutral for our current benchmarks, but I did write a new benchmark to demonstrate the improvement. This benchmark works because it measures the time to evaluate the compiled LWC template, rather than the time to render it. The improvement is 2-6%:

Screenshot 2024-07-31 at 4 12 08 PM

This number is actually an undercount, since the benchmark actually measures the time to download the .js file as well as the time to evaluate it:

Screenshot 2024-07-31 at 4 22 34 PM

(Sadly I could not get this benchmark to work in Best, which is why I'm not committing it to master. Also it's not very good due to the above issue with network vs evaluation time. Deferred import evaluation would help a lot here.)

Tests

I also added a bunch of new Karma tests to ensure I didn't break anything. The tests pass with and without this PR.

Does this pull request introduce a breaking change?

  • 😮‍💨 No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • 🤞 No, it does not introduce an observable change.

@nolanlawson nolanlawson requested a review from a team as a code owner July 31, 2024 23:34
}

// Mapping of cacheKeys to token array (assumed to come from a tagged template literal) to an Element
const fragmentCache: Map<number, WeakMap<string[], Element>> = new Map();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered just creating 4 WeakMaps but it didn't seem to offer a perf improvement.

function buildParseFragmentFn(
createFragmentFn: (html: string, renderer: RendererAPI) => Element
): (strings: string[], ...keys: (string | number)[]) => () => Element {
return (strings: string[], ...keys: (string | number)[]) => {
const cache = create(null);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this cache was effectively tied to every tagged template literal. Each tagged template literal has a unique string[] object, so using the string[] as the cache key in a WeakMap effectively gives us a unique-per-tagged-template-literal cache.


return function (parts?: VStaticPart[]): Element {
return function parseFragment(strings: string[], ...keys: (string | number)[]) {
return function applyFragmentParts(parts?: VStaticPart[]): Element {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to name these functions because it's easier to read in a perf trace.

// Cache is only here to prevent calling innerHTML multiple times which doesn't happen on the server.
if (process.env.IS_BROWSER) {
setInFragmentCache(cacheKey, strings, element);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small bug I noticed – the fragment cache is only ever used in the browser, but we were still (uselessly) setting it here even on the server.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Would it make sense to put the browser check within the function so that we don't forget about it in other places?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: 3fdcaf4

@nolanlawson
Copy link
Collaborator Author

/nucleus test

Copy link
Contributor

Well that's strange! A workflow could not be found for this PR. Please try running the /nucleus start command to start the workflow.

@nolanlawson
Copy link
Collaborator Author

/nucleus start

new WeakMap(),
new WeakMap(),
new WeakMap(),
];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit embarrassing, but using ArrayFill/ArrayFrom is really gnarly given our @lwc/shared conventions. This is a lot more readable than those options.

Copy link
Contributor

@wjhsf wjhsf Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayFrom({length: 4}, () => new WeakMap())

Doesn't seem too gnarly to me. 🤷

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't know about that shorthand where you pass in the function as the second arg! You're right, that's much nicer.

d437138

new WeakMap(),
new WeakMap(),
new WeakMap(),
];
Copy link
Contributor

@wjhsf wjhsf Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayFrom({length: 4}, () => new WeakMap())

Doesn't seem too gnarly to me. 🤷

// Cache is only here to prevent calling innerHTML multiple times which doesn't happen on the server.
if (process.env.IS_BROWSER) {
setInFragmentCache(cacheKey, strings, element);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Would it make sense to put the browser check within the function so that we don't forget about it in other places?

@nolanlawson nolanlawson merged commit fd3c9e6 into master Aug 6, 2024
11 checks passed
@nolanlawson nolanlawson deleted the nolan/perf-fragment-cache branch August 6, 2024 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants