From 0a6e8287bc9e0f952d410846897513a305152729 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Sat, 27 Dec 2025 14:44:56 +0900 Subject: [PATCH 1/6] Add LRU cache utility module 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. --- lib/utils/cache.js | 40 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 3 ++- package.json | 3 ++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 lib/utils/cache.js diff --git a/lib/utils/cache.js b/lib/utils/cache.js new file mode 100644 index 00000000..6a614ee0 --- /dev/null +++ b/lib/utils/cache.js @@ -0,0 +1,40 @@ +"use strict"; + +const { LRUCache } = require("lru-cache"); + +// Instance of the LRU Cache. Stores up to 4096 items. +const lruCache = new LRUCache({ + max: 4096 +}); + +// A sentinel symbol used to store null values, as lru-cache does not support nulls. +const nullSentinel = Symbol("null"); + +/** + * Sets a value in the cache for the given key. + * If the value is null, it is internally stored as `nullSentinel`. + * + * @param {string|number} key - The cache key. + * @param {any} value - The value to be cached. + * @returns {void} + */ +const setCache = (key, value) => { + lruCache.set(key, value === null ? nullSentinel : value); +}; + +/** + * Retrieves the cached value associated with the given key. + * If the stored value is `nullSentinel`, it returns null. + * + * @param {string|number} key - The cache key. + * @returns {any|null|undefined} The cached item, undefined if the key does not exist. + */ +const getCache = (key) => { + const value = lruCache.get(key); + return value === nullSentinel ? null : value; +}; + +module.exports = { + getCache, + setCache +}; diff --git a/package-lock.json b/package-lock.json index 068408b1..3c7a278d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0" + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" }, "devDependencies": { "@babel/generator": "^7.28.5", diff --git a/package.json b/package.json index 53e44eec..9c66136a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0" + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" }, "devDependencies": { "@babel/generator": "^7.28.5", From bb02056e0daccfa2f1d181927356b8c038134df3 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Sat, 27 Dec 2025 15:16:51 +0900 Subject: [PATCH 2/6] Add caching to property value parsing functions 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. --- lib/parsers.js | 208 +++++++++++++++++++++++++++++-------------------- 1 file changed, 123 insertions(+), 85 deletions(-) diff --git a/lib/parsers.js b/lib/parsers.js index 4704a22a..50162770 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -6,6 +6,7 @@ const { } = require("@asamuzakjp/css-color"); const { next: syntaxes } = require("@csstools/css-syntax-patches-for-csstree"); const csstree = require("css-tree"); +const { getCache, setCache } = require("./utils/cache"); const { asciiLowercase } = require("./utils/strings"); // CSS global keywords @@ -192,16 +193,25 @@ const isValidPropertyValue = (prop, val) => { } return false; } - let ast; + const cacheKey = `isValidPropertyValue_${prop}_${val}`; + const cachedValue = getCache(cacheKey); + if (typeof cachedValue === "boolean") { + return cachedValue; + } + let result; try { - ast = parseCSS(val, { - context: "value" + const ast = parseCSS(val, { + options: { + context: "value" + } }); + const { error, matched } = cssTree.lexer.matchProperty(prop, ast); + result = error === null && matched !== null; } catch { - return false; + result = false; } - const { error, matched } = cssTree.lexer.matchProperty(prop, ast); - return error === null && matched !== null; + setCache(cacheKey, result); + return result; }; /** @@ -218,6 +228,11 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => { if (val === "" || hasVarFunc(val) || !hasCalcFunc(val)) { return val; } + const cacheKey = `resolveCalc_${val}`; + const cachedValue = getCache(cacheKey); + if (typeof cachedValue === "string") { + return cachedValue; + } const obj = parseCSS(val, { context: "value" }, true); if (!obj?.children) { return; @@ -243,7 +258,9 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => { values.push(itemName ?? itemValue); } } - return values.join(" "); + const resolvedValue = values.join(" "); + setCache(cacheKey, resolvedValue); + return resolvedValue; }; /** @@ -269,123 +286,144 @@ const parsePropertyValue = (prop, val, opt = {}) => { } val = calculatedValue; } + const cacheKey = `parsePropertyValue_${prop}_${val}_${caseSensitive}`; + const cachedValue = getCache(cacheKey); + if (cachedValue === false) { + return; + } else if (inArray) { + if (Array.isArray(cachedValue)) { + return cachedValue; + } + } else if (typeof cachedValue === "string") { + return cachedValue; + } + let parsedValue; const lowerCasedValue = asciiLowercase(val); if (GLOBAL_KEYS.has(lowerCasedValue)) { if (inArray) { - return [ + parsedValue = [ { type: AST_TYPES.GLOBAL_KEYWORD, name: lowerCasedValue } ]; + } else { + parsedValue = lowerCasedValue; } - return lowerCasedValue; } else if (SYS_COLORS.has(lowerCasedValue)) { if (/^(?:(?:-webkit-)?(?:[a-z][a-z\d]*-)*color|border)$/i.test(prop)) { if (inArray) { - return [ + parsedValue = [ { type: AST_TYPES.IDENTIFIER, name: lowerCasedValue } ]; + } else { + parsedValue = lowerCasedValue; } - return lowerCasedValue; - } - return; - } - try { - const ast = parseCSS(val, { - context: "value" - }); - const { error, matched } = cssTree.lexer.matchProperty(prop, ast); - if (error || !matched) { - return; + } else { + parsedValue = false; } - if (inArray) { - const obj = cssTree.toPlainObject(ast); - const items = obj.children; - const parsedValues = []; - for (const item of items) { - const { children, name, type, value, unit } = item; - switch (type) { - case AST_TYPES.DIMENSION: { - parsedValues.push({ - type, - value, - unit: asciiLowercase(unit) - }); - break; - } - case AST_TYPES.FUNCTION: { - const css = cssTree - .generate(item) - .replace(/\)(?!\)|\s|,)/g, ") ") - .trim(); - const raw = items.length === 1 ? val : css; - // Remove "${name}(" from the start and ")" from the end - const itemValue = raw.slice(name.length + 1, -1).trim(); - if (name === "calc") { - if (children.length === 1) { - const [child] = children; - if (child.type === AST_TYPES.NUMBER) { - parsedValues.push({ - type: AST_TYPES.CALC, - isNumber: true, - value: `${parseFloat(child.value)}`, - name, - raw - }); + } else { + try { + const ast = parseCSS(val, { + context: "value" + }); + const { error, matched } = cssTree.lexer.matchProperty(prop, ast); + if (error || !matched) { + parsedValue = false; + } else if (inArray) { + const obj = cssTree.toPlainObject(ast); + const items = obj.children; + const values = []; + for (const item of items) { + const { children, name, type, value, unit } = item; + switch (type) { + case AST_TYPES.DIMENSION: { + values.push({ + type, + value, + unit: asciiLowercase(unit) + }); + break; + } + case AST_TYPES.FUNCTION: { + const css = cssTree + .generate(item) + .replace(/\)(?!\)|\s|,)/g, ") ") + .trim(); + const raw = items.length === 1 ? val : css; + // Remove "${name}(" from the start and ")" from the end + const itemValue = raw.slice(name.length + 1, -1).trim(); + if (name === "calc") { + if (children.length === 1) { + const [child] = children; + if (child.type === AST_TYPES.NUMBER) { + values.push({ + type: AST_TYPES.CALC, + isNumber: true, + value: `${parseFloat(child.value)}`, + name, + raw + }); + } else { + values.push({ + type: AST_TYPES.CALC, + isNumber: false, + value: `${asciiLowercase(itemValue)}`, + name, + raw + }); + } } else { - parsedValues.push({ + values.push({ type: AST_TYPES.CALC, isNumber: false, - value: `${asciiLowercase(itemValue)}`, + value: asciiLowercase(itemValue), name, raw }); } } else { - parsedValues.push({ - type: AST_TYPES.CALC, - isNumber: false, - value: asciiLowercase(itemValue), + values.push({ + type, name, + value: asciiLowercase(itemValue), raw }); } - } else { - parsedValues.push({ - type, - name, - value: asciiLowercase(itemValue), - raw - }); + break; } - break; - } - case AST_TYPES.IDENTIFIER: { - if (caseSensitive) { - parsedValues.push(item); - } else { - parsedValues.push({ - type, - name: asciiLowercase(name) - }); + case AST_TYPES.IDENTIFIER: { + if (caseSensitive) { + values.push(item); + } else { + values.push({ + type, + name: asciiLowercase(name) + }); + } + break; + } + default: { + values.push(item); } - break; - } - default: { - parsedValues.push(item); } } + parsedValue = values; + } else { + parsedValue = val; } - return parsedValues; + } catch { + parsedValue = false; } - } catch { + } + setCache(cacheKey, parsedValue); + if (parsedValue === false) { return; } - return val; + return parsedValue; }; /** From 8622e7a584d342e0f60c080b5e708df0a5bc02c3 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Sat, 27 Dec 2025 16:53:02 +0900 Subject: [PATCH 3/6] Sort exports Rearranged the order of exported functions in module.exports for better organization and consistency. No functional changes were made. --- lib/parsers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/parsers.js b/lib/parsers.js index 50162770..bcd0c74a 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -643,6 +643,7 @@ const parseGradient = (val) => { } }; +<<<<<<< HEAD /** * Resolves a keyword value. * From a4bf2a179b4b40d8b976a9cc85c82a0d25efb467 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Wed, 31 Dec 2025 15:47:51 +0900 Subject: [PATCH 4/6] Remove null sentinel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) ``` --- lib/utils/cache.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/utils/cache.js b/lib/utils/cache.js index 6a614ee0..09aeed06 100644 --- a/lib/utils/cache.js +++ b/lib/utils/cache.js @@ -7,19 +7,15 @@ const lruCache = new LRUCache({ max: 4096 }); -// A sentinel symbol used to store null values, as lru-cache does not support nulls. -const nullSentinel = Symbol("null"); - /** * Sets a value in the cache for the given key. - * If the value is null, it is internally stored as `nullSentinel`. * * @param {string|number} key - The cache key. * @param {any} value - The value to be cached. * @returns {void} */ const setCache = (key, value) => { - lruCache.set(key, value === null ? nullSentinel : value); + lruCache.set(key, value); }; /** @@ -27,11 +23,10 @@ const setCache = (key, value) => { * If the stored value is `nullSentinel`, it returns null. * * @param {string|number} key - The cache key. - * @returns {any|null|undefined} The cached item, undefined if the key does not exist. + * @returns {any|undefined} The cached item, undefined if the key does not exist. */ const getCache = (key) => { - const value = lruCache.get(key); - return value === nullSentinel ? null : value; + return lruCache.get(key); }; module.exports = { From 88f76d879a2e71495643b73c2648bded28ce8a96 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Wed, 31 Dec 2025 17:11:14 +0900 Subject: [PATCH 5/6] Add lru-cache in parsers.js --- lib/parsers.js | 20 ++++++++++++-------- lib/utils/cache.js | 35 ----------------------------------- 2 files changed, 12 insertions(+), 43 deletions(-) delete mode 100644 lib/utils/cache.js diff --git a/lib/parsers.js b/lib/parsers.js index bcd0c74a..cb43e4d1 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -6,7 +6,7 @@ const { } = require("@asamuzakjp/css-color"); const { next: syntaxes } = require("@csstools/css-syntax-patches-for-csstree"); const csstree = require("css-tree"); -const { getCache, setCache } = require("./utils/cache"); +const { LRUCache } = require("lru-cache"); const { asciiLowercase } = require("./utils/strings"); // CSS global keywords @@ -87,6 +87,11 @@ const varContainedRegEx = /(?<=[*/\s(])var\(/; // Patched css-tree const cssTree = csstree.fork(syntaxes); +// Instance of the LRU Cache. Stores up to 4096 items. +const lruCache = new LRUCache({ + max: 4096 +}); + /** * Prepares a stringified value. * @@ -194,7 +199,7 @@ const isValidPropertyValue = (prop, val) => { return false; } const cacheKey = `isValidPropertyValue_${prop}_${val}`; - const cachedValue = getCache(cacheKey); + const cachedValue = lruCache.get(cacheKey); if (typeof cachedValue === "boolean") { return cachedValue; } @@ -210,7 +215,7 @@ const isValidPropertyValue = (prop, val) => { } catch { result = false; } - setCache(cacheKey, result); + lruCache.set(cacheKey, result); return result; }; @@ -229,7 +234,7 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => { return val; } const cacheKey = `resolveCalc_${val}`; - const cachedValue = getCache(cacheKey); + const cachedValue = lruCache.get(cacheKey); if (typeof cachedValue === "string") { return cachedValue; } @@ -259,7 +264,7 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => { } } const resolvedValue = values.join(" "); - setCache(cacheKey, resolvedValue); + lruCache.set(cacheKey, resolvedValue); return resolvedValue; }; @@ -287,7 +292,7 @@ const parsePropertyValue = (prop, val, opt = {}) => { val = calculatedValue; } const cacheKey = `parsePropertyValue_${prop}_${val}_${caseSensitive}`; - const cachedValue = getCache(cacheKey); + const cachedValue = lruCache.get(cacheKey); if (cachedValue === false) { return; } else if (inArray) { @@ -419,7 +424,7 @@ const parsePropertyValue = (prop, val, opt = {}) => { parsedValue = false; } } - setCache(cacheKey, parsedValue); + lruCache.set(cacheKey, parsedValue); if (parsedValue === false) { return; } @@ -643,7 +648,6 @@ const parseGradient = (val) => { } }; -<<<<<<< HEAD /** * Resolves a keyword value. * diff --git a/lib/utils/cache.js b/lib/utils/cache.js deleted file mode 100644 index 09aeed06..00000000 --- a/lib/utils/cache.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -const { LRUCache } = require("lru-cache"); - -// Instance of the LRU Cache. Stores up to 4096 items. -const lruCache = new LRUCache({ - max: 4096 -}); - -/** - * Sets a value in the cache for the given key. - * - * @param {string|number} key - The cache key. - * @param {any} value - The value to be cached. - * @returns {void} - */ -const setCache = (key, value) => { - lruCache.set(key, value); -}; - -/** - * Retrieves the cached value associated with the given key. - * If the stored value is `nullSentinel`, it returns null. - * - * @param {string|number} key - The cache key. - * @returns {any|undefined} The cached item, undefined if the key does not exist. - */ -const getCache = (key) => { - return lruCache.get(key); -}; - -module.exports = { - getCache, - setCache -}; From 1b2c0b87aa9b30a4d4460c8467f71b5b75548763 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Wed, 31 Dec 2025 18:00:38 +0900 Subject: [PATCH 6/6] Update parsers.js --- lib/parsers.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/parsers.js b/lib/parsers.js index cb43e4d1..074f857e 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -206,9 +206,7 @@ const isValidPropertyValue = (prop, val) => { let result; try { const ast = parseCSS(val, { - options: { - context: "value" - } + context: "value" }); const { error, matched } = cssTree.lexer.matchProperty(prop, ast); result = error === null && matched !== null;