-
-
Notifications
You must be signed in to change notification settings - Fork 81
Add LRU cache for CSS parsing and validation #271
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
Conversation
lib/utils/cache.js
Outdated
| max: 4096 | ||
| }); | ||
|
|
||
| // A sentinel symbol used to store null values, as lru-cache does not support nulls. |
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 don't think this is actually necessary. Test:
"use strict";
const { LRUCache } = require("lru-cache");
const cache = new LRUCache({ max: 4096 });
cache.set("abc", null);
console.log(cache.get("abc"));
console.log(cache.get("def"));prints null, then undefined.
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.
Removed null sentinel.
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.
Can you just remove the entire cache.js file and create a new LRUCache at the top of parsers.js instead?
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.
benchmark:
# style/createElement + setAttribute('style') #
jsdom x 172 ops/sec ±1.48% (81 runs sampled)
# style/createElement + style properties #
jsdom x 228 ops/sec ±3.03% (73 runs sampled)
# style/createElement + style.cssText #
jsdom x 154 ops/sec ±3.24% (78 runs sampled)
# style/innerHTML: divs with inline styles #
jsdom x 144 ops/sec ±3.02% (74 runs sampled)
# style/innerHTML: simple divs (no styles) #
jsdom x 4,028 ops/sec ±3.59% (89 runs sampled)
Introduced a new cache utility in lib/utils/cache.js using lru-cache to store up to 4096 items, with support for caching null values. Added lru-cache as a dependency in package.json.
Introduces caching for isValidPropertyValue, resolveCalc, and parsePropertyValue to improve performance by avoiding redundant computations. Utilizes getCache and setCache from utils/cache to store and retrieve results based on input parameters.
Rearranged the order of exported functions in module.exports for better organization and consistency. No functional changes were made.
benchmark result:
```
> jsdom@27.4.0 benchmark
> node ./benchmark/runner --suites style
# style/createElement + setAttribute('style') #
jsdom x 170 ops/sec ±1.30% (81 runs sampled)
# style/createElement + style properties #
jsdom x 246 ops/sec ±3.05% (80 runs sampled)
# style/createElement + style.cssText #
jsdom x 141 ops/sec ±2.70% (79 runs sampled)
# style/innerHTML: divs with inline styles #
jsdom x 154 ops/sec ±2.26% (79 runs sampled)
# style/innerHTML: simple divs (no styles) #
jsdom x 3,756 ops/sec ±2.11% (87 runs sampled)
```
|
Once this PR is merged, please release a new cssstyle and jsdom. |
| try { | ||
| const ast = parseCSS(val, { | ||
| context: "value" | ||
| }); |
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.
Sorry, I just noticed. Why does this use { context: "value" } but the other one use { options: { context: "value" } }? Is there a hidden bug here?
(I was wondering if we might get some performance by caching parseCSS() results, i.e., sharing the cache between parsePropertyValue and isValidPropertyValue. But, that could be a followup anyway, so it is not blocking.)
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.
Thanks for the catch, was a typo...
{ context: "value" } is the correct option for csstree.
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 worry a bit that our test coverage didn't catch this, but oh well...
|
FYI
For example, if you apply caching to parseCSS: const parseCSS = (val, opt, toObject = false) => {
if (typeof val !== "string") {
val = prepareValue(val);
}
const cacheKey = `parseCSS_${val}_${opt?.context}_${toObject}`;
const cachedValue = lruCache.get(cacheKey);
if (cachedValue) {
return cachedValue;
}
let result;
const ast = cssTree.parse(val, opt);
if (toObject) {
result = cssTree.toPlainObject(ast);
} else {
result = ast;
}
lruCache.set(cacheKey, result);
return result;
};benchmark: There's a slight improvement. |
|
I think we should only do it if we can eliminate the separate parsePropertyValue and isValidPropertyValue caches at the same time, and still get good results. (Even if they are slightly less good than the current version.) One thing that can be difficult about this sort of work is that by running a single benchmark that runs the same code over and over, we are hitting the best case for a LRU cache. But in the real world, people might be running code that fills up some of the cache with parsePropertyValue results, and then some of it with isValidPropertyValue results, in a way that causes the parsePropertyValue results to get evicted. So as a general principle, if we can reduce the amount of competition for cache entries, that would likely be a good direction. (That is, we would move from 3 types of cache entries: isValidPropertyValue, parsePropertyValue, and resolveCalc, to 2 types: parseCSS and resolveCalc.) |
|
Well then, let's wait and see. |
Summary
Adds an LRU cache to reduce repeated css-tree parsing/validation and improve performance of CSS parsing utilities.
Part of jsdom/jsdom#3985
What this PR does
Benchmarks
cssstyle main:
cssstyle PR: