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
Cache element styles for getComputedStyle #3482
Conversation
9f7800f
to
c078fd5
Compare
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.
Really great work. Some suggestions for how to clean it up.
Bonus points (but not required) if you add extra to-upstream WPT coverage as follows:
- Remove all current calls to invalidateStyleCache
- For each such current call, add a test that fails. Then, add the call back, and ensure the new test passes.
- Finally, consolidate all the calls as I suggested here into
_modified
, instead of having them be spread out like they are here.
All such tests could be in the same file, for simplicity.
I think at least one such test would help a lot, by providing a template in case we notice future regressions or over-caching instances.
lib/jsdom/living/attributes.js
Outdated
@@ -30,6 +31,7 @@ exports.changeAttribute = (element, attribute, value) => { | |||
const { _localName, _namespace, _value } = attribute; | |||
|
|||
queueAttributeMutationRecord(element, _localName, _namespace, _value); | |||
invalidateStyleCache(element); |
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 all, or maybe almost-all, invalidStyleCache
calls can be moved into
jsdom/lib/jsdom/living/nodes/Node-impl.js
Line 215 in 22f7c3c
_modified() { |
const styleCache = new WeakMap(); | ||
|
||
exports.invalidateStyleCache = elementImpl => { | ||
if (elementImpl._ownerDocument) { |
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.
_ownerDocument should never be null. (Here and elsewhere.)
@@ -44,6 +45,58 @@ exports.forEachMatchingSheetRuleOfElement = (elementImpl, handleRule) => { | |||
|
|||
handleSheet(parsedDefaultStyleSheet); | |||
forEach.call(elementImpl._ownerDocument.styleSheets._list, handleSheet); | |||
} | |||
|
|||
const styleCache = new WeakMap(); |
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.
Instead of a weak map, why not put this on the document itself? Combined with the non-nullness of _ownerDocument
, that could greatly reduce the lines of code needed to access the style cache. Just have the weak map always exist.
@@ -79,6 +80,7 @@ class CharacterDataImpl extends NodeImpl { | |||
} | |||
|
|||
queueMutationRecord(MUTATION_TYPE.CHARACTER_DATA, this, null, null, this._data, [], [], null, null); | |||
invalidateStyleCache(this); |
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'm not sure this is necessary? When could changing text data cause a style update?
Thanks @domenic for your feedback! I need to finish some other tasks before I get back to this, but I plan to implement the TDD-style tests as you suggested, and will try if moving the invalidation to the
I didn't know that, it's true that it will help to simplify the implementation quite a bit.
There's the |
c078fd5
to
76f7dc1
Compare
Hi @domenic 👋 I updated the PR according to your feedback. The style cache is now directly on the Also, I implemented a test suite, gradually adding tests for things like attribute or node changes, and adding
I found one case where text data change can trigger style change. Try this in a browser: const el = document.createElement("div");
el.textContent = "foo";
document.body.append(el); Now try el.childNodes[0].data = ""; This makes the node empty, although it still has one child text node, and However, JSDOM currently implements this incorrectly, because Merely setting To make this forward-compatible, I also invalidate styles in While working on the PR, I discovered a bug: when a When a stylesheet is removed or created, that's another place where we need to invalidate style cache. |
Here's how Chromium implements the It really looks at the character data of child text nodes and checks if they are empty. There's also preparatory code to start accepting also whitespace-only text as |
76f7dc1
to
aa228a5
Compare
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.
Really great work; thank you so much!
@domenic Do you think this could be backported to the 20.x.x line, so that users of Jest can get the performance improvements right away? Otherwise we'll need to wait for |
No, sorry, we only have bandwidth to maintain a single release of jsdom. I suggest working with the maintainers of that other package. |
For record, this is a Jest PR to upgrade |
Fixes #3234. Caches the computed styles for each element, avoiding expensive re-calculations. Also, modifies the implementation of
getResolvedValue
to not match all the style rules for itself, but reuse the cached results.Each document has its own cache, and the cache is completely invalidated on every DOM change. I looked up all
queueMutationRecord
calls and added aninvalidateStyleCache
call to every place that also triggers a mutation observer.What are the actual performance improvements? I tested it on one of our React Testing Library unit test suites in the Gutenberg project. Before the patch, the suite takes 11.0 s to run:
After the patch, it takes only 7.6 s to run, a 30% improvement:
There's one test that's particularly heavy on
getComputedStyle
calls:This one improves from 2.90s to 0.73s, an 75% improvement, or 4x faster.
The
pointer-events
patch in #3478 adds more perf improvements on top of that. The test run looks like:The 5.2 s time, compared to 7.6 s without the
pointer-events
patch, means another 30% improvement. Due to faster style lookup duringuserEvent.click
.