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: special handling for static elements #2781

Merged
merged 3 commits into from
Jun 9, 2022

Conversation

jodarove
Copy link
Contributor

@jodarove jodarove commented Apr 7, 2022

Details

Perf

image

The regressions are from 1k-different-style tests, and are because the component under test template is static, and it needs to replace the style token.

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

Because class and style attributes of static nodes are not parsed into classMap and styleMap, it may be the case that they may contain extra characters in ssr and the dom. ex: <div class=" foo bar"> now generates exactly that, while before this pr it used to generate <div class="foo bar">

@jodarove jodarove closed this Apr 7, 2022
@jodarove jodarove reopened this Apr 7, 2022
@jodarove jodarove marked this pull request as draft April 7, 2022 13:15
@jodarove jodarove force-pushed the jodarove/caridy-create-fragment-approach branch from 09c68eb to 4438a7d Compare April 7, 2022 13:17
@jodarove jodarove requested review from pmdartus, nolanlawson and caridy and removed request for pmdartus April 7, 2022 13:18
@jodarove jodarove force-pushed the jodarove/caridy-create-fragment-approach branch from 4438a7d to 1af0703 Compare April 7, 2022 16:17
sel: undefined,
key,
elm: undefined,
elmProto,
Copy link
Contributor

Choose a reason for hiding this comment

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

this property should probably be renamed to frag or fragment, using Proto postfix is misleading.

@@ -49,6 +51,18 @@ function addVNodeToChildLWC(vnode: VCustomElement) {
ArrayPush.call(getVMBeingRendered()!.velements, vnode);
}

// [st]atic node
function st(elmProto: Element, key: Key): VStatic {
Copy link
Contributor

Choose a reason for hiding this comment

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

why not reusing VElementData here? and take the key out of the data? My hunch is that eventually we might be able to do some crazy optimizations where the data payload can provide some stuff for the subtree, so it makes sense to keep it around.

Copy link
Contributor Author

@jodarove jodarove Apr 7, 2022

Choose a reason for hiding this comment

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

atm is not needed and this is internals that we could modify to add the data bag once we have an optimization in mind. passing the key directly will reduce size (fewer chars), and perf (destructing key from the databag). however, I can generate the databag if that is preferred.

Copy link
Contributor

@caridy caridy left a comment

Choose a reason for hiding this comment

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

Looking good... simpler that I thought it was going to be. Minor details to figure out and some other silly questions :)


htmlFragments.push(strings[strings.length - 1]);

return createFragment(htmlFragments.join(''));
Copy link
Contributor Author

@jodarove jodarove Apr 7, 2022

Choose a reason for hiding this comment

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

Note: I don't like the extra ""="", or the class="". what's your opinion about doing about stripping them away? at least in dev or test.

Notice that we can fix the errors in our repo, but I figure that there will be other repos with tests in the same situation.

Alternatively, what about doing our own serializer (sounds difficult)? to work around the fact that parse5 attributes value always needs to be defined (we can't generate `<div ${1}>).

Copy link
Contributor

Choose a reason for hiding this comment

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

I hope that we can avoid ""="", or the class="", let's chat

Copy link
Contributor Author

@jodarove jodarove Apr 10, 2022

Choose a reason for hiding this comment

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

resolved in 706a6fd (adds a custom serializer for static elements using our ast)

Copy link
Collaborator

Choose a reason for hiding this comment

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

My intuition tells me that it would be faster at runtime to create elements and add attributes (i.e. document.createElement, appendChild, setAttribute, etc.), rather than use createContextualFragment. It would avoid parsing HTML on the client-side.

However, the tradeoff is that the current approach has a more compact code representation. And for very large static content (e.g. lots of nested elements), I think creating the fragment may actually be faster at runtime. So this seems like a good approach to me.

@nolanlawson
Copy link
Collaborator

Reviewers note: the snapshot tests have not been updated to ease up the review.

FWIW, I usually just use the "file filter" in the GitHub UI to filter out .js files so I don't see the snapshots. Typically our source files are only .ts, not .js.

@nolanlawson
Copy link
Collaborator

This looks reasonable to me; it feels a bit simpler and more straightforward than the other PR. I'm curious to know what the perf improvement looks like?

Nice work!

@jodarove jodarove force-pushed the jodarove/caridy-create-fragment-approach branch 2 times, most recently from 81abb4d to 941ce71 Compare April 10, 2022 18:45
@nolanlawson
Copy link
Collaborator

Confirmed that 95ce4c3 broke the test in this PR.

@nolanlawson
Copy link
Collaborator

Did an analysis of downstream Nucleus tests. Seems there are only 2 new failures, and they are due to minor snapshot differences.

@nolanlawson
Copy link
Collaborator

Blocked by #2859.

@nolanlawson
Copy link
Collaborator

Looks like this PR was severely bitrotted by da49079. Merge conflicts galore.

In particular, this part seems problematic now:

// Note: at the moment this code executes, createFragment have not being set.
export const parseFragment = buildParseFragmentFn((html) => createFragment(html));
export const parseSVGFragment = buildParseFragmentFn((html) => {
const fragment = createFragment('<svg>' + html + '</svg>');
return getFirstChild(fragment);
});

We don't have access to the renderer anymore in @lwc/engine-core, so I'm not sure how this is supposed to work anymore.

// Engine-core public APIs -------------------------------------------------------------------------
export { parseFragment as parseSVGFragment };
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we reuse the same parseFragment in server?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like the reason is because parseSVGFragment uses getFirstChild, which is not supported in the server environment:

export const parseSVGFragment = buildParseFragmentFn((html) => {
const fragment = createFragment('<svg>' + html + '</svg>');
return getFirstChild(fragment);
});

If I try to use the actual parseSVGFragment, I get an error in the Jest tests:

Click to see
Summary of all failing tests
 FAIL  packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts (18.993 s)
  ● fixtures › svgs/index.js

    TypeError: "getFirstChild" is not supported in this environment

      13 |     create,
      14 |     StringToLowerCase,
    > 15 |     htmlPropertyToAttribute,
         |               ^
      16 |     noop,
      17 | } from '@lwc/shared';
      18 |

      at src/renderer.ts:15:15
      at ../engine-core/dist/engine-core.cjs.js:4654:12
      at ../engine-core/dist/engine-core.cjs.js:4645:31
      at tmpl (src/__tests__/fixtures/svgs/dist/compiled.js:35:27)
      at ../engine-core/dist/engine-core.cjs.js:4718:27
      at ReactiveObserver.observe (../engine-core/dist/engine-core.cjs.js:339:13)
      at isUpdatingTemplate (../engine-core/dist/engine-core.cjs.js:4674:13)
      at runWithBoundaryProtection (../engine-core/dist/engine-core.cjs.js:5534:5)
      at evaluateTemplate (../engine-core/dist/engine-core.cjs.js:4667:5)
      at invokeComponentRenderMethod (../engine-core/dist/engine-core.cjs.js:4820:41)

jodarove and others added 2 commits June 7, 2022 16:28
wip: add a static vnode type

wip: needs key and patch

wip: all tests passing exept a few because of simple things

wip: set style tokens for fragments during template evaluation

wip: set element shadow token for fragments during template evaluation

wip: propagate the shadow resolver key of static fragments

wip: do not gen static nodes for text or comments

wip: use tagedTemplate expression to replate stylesheetToken

wip: use cloneNode from renderer

wip: treeWalker to work in ie11

refactor: do not strip empty attr or empty class attr

fix: using incorrect key

wip: trim value of textNodes and review feedback

fix: hydration

feat: custom static element serializer

wip: remove unessesary import

fix: hydration

fix: snapshot tests

fix: missing karma test

fix: test due rebase

test: add test for static content needing nodeOwner

fix: escape strings in serializer

refactor: remove unused apis on generated code

refactor: review suggestions

fix: support mixed mode

wip: fix compilation snapshots

fix: increase 0.5kb bundlesize for engine dom

fix: flapper

wip: helpers.ts review

wip: codegen.ts review

wip: missing items from pm review

wip: review comments

fix: respect preserveComments and fuse $1,2 into 3

fix: svg content with the correct namespace

feat(template-compiler): add option to disable static content optimizations

wip: remove invalid comment

chore: bump version to v2.13.0 (#2784)

chore: dependencies upgrade (#2785)

test: fix Node warning about event emitters (#2789)

chore: run karma and integration tests in parallel (#2792)

* chore: run karma and integration tests in parallel

* fix: remove log lines

fix(babel-plugin-component): remove import validation (#2719)

test: remove flakey IE integration test (#2796)

test: update test to use lwc imports (#2794)

chore: Restrict further import order (#2795)

chore: bump version to v2.13.1 (#2804)

refactor(engine): moving vm references from dom into core (#2801)

* refactor(engine): moving vm references from dom into core

chore(nucleus): remove salesforcedevs/developer-website (#2807)

test(integration-karma): small quality-of-life improvements (#2809)

chore(deps): bump ejs from 3.1.6 to 3.1.7 (#2810)

chore: weekly dependencies upgrade (#2816)

* chore: weekly dependencies upgrade

* fix: update yarn.lock`

refactor(engine): optimize computation of transitive shadow mode (#2803)

chore(deps): bump async from 2.6.3 to 2.6.4 (#2815)

Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](caolan/async@v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

chore: bump version to v2.13.2 (#2819)

chore: retry failed Circle CI tests (#2814)

* chore: retry failed Circle CI tests

W-10946477

* chore: fix

* chore: fix

* chore: fix

* chore: fix

* chore: fix

* chore: fix

* chore: fix

* chore: deliberately fail a test to see what happens

* chore: improve retry script

* fix: whitespace

* Revert "chore: deliberately fail a test to see what happens"

This reverts commit 611fc34.

* chore: rename to retry_karma

fix(engine-core): add shim for old stylesheetTokens internal API (#2821)

W-11093934

chore: bump version to v2.13.3 (#2823)

fix(build): remove swc, switch back to babel and terser (#2818)

feat: add freezeTemplate() API, warn on mutation (#2825)

* feat: add freezeTemplate() API, warn on mutation

* fix: warn on slots/renderMode as well

* fix: add comment

* fix: remove duplicate process.env.NODE_ENV check

fix(engine-dom): refactor stylesheet API for consistency (#2827)

* fix(engine-dom): refactor stylesheet API for consistency

* fix: remove useless code comment

* test: remove unnecessary test

* test: remove unnecessary test

* refactor: slight refactor

* fix: add code comments

* fix: add code comments

* fix: add better comment

fix: relax static id validation in iterations (#2830)

fix(rollup-plugin): emit warnings during compilation (#2833)

* fix(rollup-plugin): emit warnings during compilation

Fixes #2771

W-10930894

* fix: add code comment

fix(engine-dom): make feature flags work (#2812)

* fix(engine-dom): make feature flags work

Fixes #2811

* fix: license headers

* test: fix jest tests

* test: fix test

* test: fix test

* fix: use Eugene's technique instead

* Revert "fix: use Eugene's technique instead"

This reverts commit 72afdc0.

* fix: use Eugene's technique instead

* fix: revert unnecessary test change

* fix: revert, use the elaborate test instead

* fix: fix feature flags in engine-server as well

perf(engine-dom): refactor style cache to reduce lookups (#2832)

* perf(engine-dom): refactor style cache to reduce lookups

* fix: tidy up comments

* fix: update packages/@lwc/engine-dom/src/styles.ts

Co-authored-by: Pierre-Marie Dartus <p.dartus@salesforce.com>

* fix: remove semi

* fix: remove "used" flag

* fix: refactor

* fix: refactor

* fix: bring back "used" flag

* fix: typo

Co-authored-by: Pierre-Marie Dartus <p.dartus@salesforce.com>

chore: update deps (#2838)

test: run feature flag test code only in karma (#2835)

fix: trigger slotchange event on removing slot (#2840)

test(integration-karma): silence lwc rollup plugin warnings (#2836)

* test(integration-karma): silence lwc rollup plugin warnings

* fix: use warn API

v2.11.7 (#2842)

chore: release v2.14.0 (#2846)

fix: only remove slot children in synthetic shadow (#2843)

* fix: only remove slot children in synthetic shadow

* fix: use case block

fix: only add version mismatch test code in karma (#2852)

test(integration-karma): ensure constructable stylesheets are re-used (#2844)

* test(integration-karma): ensure constructable stylesheets are re-used

* test: add test for shared style

chore(nucleus): remove more downstreams (#2855)

chore(nucleus): remove another downstream (#2857)

docs: fix typo in template compiler readme (#2848)

* docs: fix typo in template compiler readme

* docs: rewording usage of lwc dynamic directive

Co-authored-by: Eugene Kashida <ekashida@gmail.com>

Co-authored-by: Eugene Kashida <ekashida@gmail.com>

chore: fix lint

test: refactor test, remove test covered in #2859

test: on second thought, bring test back
@nolanlawson nolanlawson force-pushed the jodarove/caridy-create-fragment-approach branch from 4d5b57e to ec12fce Compare June 7, 2022 23:28
@@ -30,6 +33,8 @@ export {
setHooks,
getComponentDef,
isComponentConstructor,
parseFragment,
Copy link
Contributor

Choose a reason for hiding this comment

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

to be removed!

}

// Note: at the moment this code executes, we don't have a renderer yet.
export const parseFragment = buildParseFragmentFn((html, renderer) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

unnecessary abstraction.

@@ -26,6 +26,8 @@ export {
setHooks,
getComponentDef,
isComponentConstructor,
parseFragment,
Copy link
Contributor

Choose a reason for hiding this comment

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

to be removed...

@@ -26,6 +26,8 @@ export {
setHooks,
getComponentDef,
isComponentConstructor,
parseFragment,
parseFragment as parseSVGFragment,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not clear why on the server side SVG can just reuse the other one... I don't remember this.

@caridy
Copy link
Contributor

caridy commented Jun 8, 2022

The biggest issue to be resolved here is the fact that, IMO, parsing HTML fragment SHOULD be considered risky all the time... meaning the template SHOULD import the renderer, and call the corresponding parsing method. Today, in this PR, it is not quite like that, what's going on now is the following:

  1. if static nodes are detected during compilation of HTML, the corresponding function is imported from LWC (parseFragment or parseSVGFragment), this function is required to be public API from LWC.
  2. The result of calling the parse** is used to pass as metadata into a static vnode.
  3. the static vnode will carry on other corresponding work to reify the fragment and clone it while using the renderer associated to the vm.

The issue that I see is that the vm will always have what we call the default renderer, meaning locker will not be able to intersect that. This is fine if we make it very clear what can be considered static, but that's more tricky to track down, I prefer to just have an always inspectable parsing mechanism by importing renderer, and calling the corresponding function from renderer that can directly parse without the extra level of abstraction.

cc @ravijayaramappa @nolanlawson

@pmdartus
Copy link
Member

pmdartus commented Jun 8, 2022

Agreed, we should import the rendererer from the engine to let Locker hook itself on it.

I prefer to just have an always inspectable parsing mechanism by importing renderer, and calling the corresponding function from renderer that can directly parse without the extra level of abstraction.

That said, I still think some level of abstraction will be necessary. At least to abstract away the current buildParseFragmentFn. This function is in charge of injecting the scope tokens and caching the fragment.

@nolanlawson
Copy link
Collaborator

parseFragment is indeed always risky. As for parseSVGFragment, this could maybe just be the same function with an option for the namespace or something. If we move this to the renderer, then that would satisfy your concerns, right @caridy?

@caridy
Copy link
Contributor

caridy commented Jun 9, 2022

parseFragment is indeed always risky. As for parseSVGFragment, this could maybe just be the same function with an option for the namespace or something. If we move this to the renderer, then that would satisfy your concerns, right @caridy?

That's correct. right now it is not sanitized, but the risk is not that big because LWC compiler parses and validates that HTML, but you never know. I'm fine to do it in another PR :)

@nolanlawson
Copy link
Collaborator

Awesome, thanks. Let's merge this, and I opened another issue to track parseFragment/parseSVGFragment: #2876

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.

4 participants