From 2eb5ef619ab506ee42682ab702d5076c37ba334f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 15 Apr 2026 12:37:21 +0200 Subject: [PATCH 1/2] crypto: optimize normalizeAlgorithm hot path Replace O(n) for...in loop with case-insensitive StringPrototypeToUpperCase comparisons per algorithm with O(1) SafeMap lookup. The map is pre-built at module init alongside kSupportedAlgorithms. Hoist the opts object literal used in normalizeAlgorithm to module level to avoid allocating identical { prefix, context } objects on every call. Pre-compute ObjectKeys() for simpleAlgorithmDictionaries entries at module init to avoid allocating a new keys array on every normalizeAlgorithm call. Signed-off-by: Filip Skokan --- lib/internal/crypto/util.js | 105 +++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 70e1027946190a..b12ed3630ef684 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -16,6 +16,7 @@ const { ObjectKeys, ObjectPrototypeHasOwnProperty, PromiseWithResolvers, + SafeMap, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -453,8 +454,11 @@ const experimentalAlgorithms = [ ]; // Transform the algorithm definitions into the operation-keyed structure +// Also builds a parallel Map per operation +// for O(1) case-insensitive algorithm name lookup in normalizeAlgorithm. function createSupportedAlgorithms(algorithmDefs) { const result = {}; + const nameMap = {}; for (const { 0: algorithmName, 1: operations } of ObjectEntries(algorithmDefs)) { // Skip algorithms that are conditionally not supported @@ -465,6 +469,8 @@ function createSupportedAlgorithms(algorithmDefs) { for (const { 0: operation, 1: dict } of ObjectEntries(operations)) { result[operation] ||= {}; + nameMap[operation] ||= new SafeMap(); + nameMap[operation].set(StringPrototypeToUpperCase(algorithmName), algorithmName); // Add experimental warnings for experimental algorithms if (ArrayPrototypeIncludes(experimentalAlgorithms, algorithmName)) { @@ -482,12 +488,14 @@ function createSupportedAlgorithms(algorithmDefs) { } } - return result; + return { algorithms: result, nameMap }; } -const kSupportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions); +const { algorithms: kSupportedAlgorithms, nameMap: kAlgorithmNameMap } = + createSupportedAlgorithms(kAlgorithmDefinitions); const simpleAlgorithmDictionaries = { + __proto__: null, AesCbcParams: { iv: 'BufferSource' }, AesCtrParams: { counter: 'BufferSource' }, AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' }, @@ -527,6 +535,12 @@ const simpleAlgorithmDictionaries = { TurboShakeParams: {}, }; +// Pre-compute ObjectKeys() for each dictionary entry at module init +// to avoid allocating a new keys array on every normalizeAlgorithm call. +for (const { 0: name, 1: types } of ObjectEntries(simpleAlgorithmDictionaries)) { + simpleAlgorithmDictionaries[name] = { keys: ObjectKeys(types), types }; +} + function validateMaxBufferLength(data, name) { if (data.byteLength > kMaxBufferLength) { throw lazyDOMException( @@ -537,6 +551,12 @@ function validateMaxBufferLength(data, name) { let webidl; +const kNormalizeAlgorithmOpts = { + __proto__: null, + prefix: 'Failed to normalize algorithm', + context: 'passed algorithm', +}; + // https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm // adapted for Node.js from Deno's implementation // https://github.com/denoland/deno/blob/v1.29.1/ext/crypto/00_crypto.js#L195 @@ -549,29 +569,20 @@ function normalizeAlgorithm(algorithm, op) { // 1. const registeredAlgorithms = kSupportedAlgorithms[op]; // 2. 3. - const initialAlg = webidl.converters.Algorithm(algorithm, { - prefix: 'Failed to normalize algorithm', - context: 'passed algorithm', - }); + const initialAlg = webidl.converters.Algorithm(algorithm, + kNormalizeAlgorithmOpts); // 4. let algName = initialAlg.name; - // 5. - let desiredType; - for (const key in registeredAlgorithms) { - if (!ObjectPrototypeHasOwnProperty(registeredAlgorithms, key)) { - continue; - } - if ( - StringPrototypeToUpperCase(key) === StringPrototypeToUpperCase(algName) - ) { - algName = key; - desiredType = registeredAlgorithms[key]; - } - } - if (desiredType === undefined) + // 5. Case-insensitive lookup via pre-built Map (O(1) instead of O(n)). + const canonicalName = kAlgorithmNameMap[op]?.get( + StringPrototypeToUpperCase(algName)); + if (canonicalName === undefined) throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + algName = canonicalName; + const desiredType = registeredAlgorithms[algName]; + // Fast path everything below if the registered dictionary is null if (desiredType === null) return { name: algName }; @@ -579,39 +590,35 @@ function normalizeAlgorithm(algorithm, op) { // 6. const normalizedAlgorithm = webidl.converters[desiredType]( { __proto__: algorithm, name: algName }, - { - prefix: 'Failed to normalize algorithm', - context: 'passed algorithm', - }, + kNormalizeAlgorithmOpts, ); // 7. normalizedAlgorithm.name = algName; - // 9. - const dict = simpleAlgorithmDictionaries[desiredType]; - // 10. - const dictKeys = dict ? ObjectKeys(dict) : []; - for (let i = 0; i < dictKeys.length; i++) { - const member = dictKeys[i]; - if (!ObjectPrototypeHasOwnProperty(dict, member)) - continue; - const idlType = dict[member]; - const idlValue = normalizedAlgorithm[member]; - // 3. - if (idlType === 'BufferSource' && idlValue) { - const isView = ArrayBufferIsView(idlValue); - normalizedAlgorithm[member] = TypedArrayPrototypeSlice( - new Uint8Array( - isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue, - isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0, - isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue), - ), - ); - } else if (idlType === 'HashAlgorithmIdentifier') { - normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); - } else if (idlType === 'AlgorithmIdentifier') { - // This extension point is not used by any supported algorithm (yet?) - throw lazyDOMException('Not implemented.', 'NotSupportedError'); + // 9. 10. Pre-computed keys and types from simpleAlgorithmDictionaries. + const dictMeta = simpleAlgorithmDictionaries[desiredType]; + if (dictMeta) { + const { keys: dictKeys, types: dictTypes } = dictMeta; + for (let i = 0; i < dictKeys.length; i++) { + const member = dictKeys[i]; + const idlType = dictTypes[member]; + const idlValue = normalizedAlgorithm[member]; + // 3. + if (idlType === 'BufferSource' && idlValue) { + const isView = ArrayBufferIsView(idlValue); + normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + new Uint8Array( + isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue, + isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0, + isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue), + ), + ); + } else if (idlType === 'HashAlgorithmIdentifier') { + normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); + } else if (idlType === 'AlgorithmIdentifier') { + // This extension point is not used by any supported algorithm (yet?) + throw lazyDOMException('Not implemented.', 'NotSupportedError'); + } } } From 8cfb8ef0f81780a4397fdd420ab6fe952adc6398 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 15 Apr 2026 12:39:25 +0200 Subject: [PATCH 2/2] crypto: reduce allocations in WebIDL dictionary converter Replace SafeArrayIterator with index-based loop in the inner function returned by createDictionaryConverter, avoiding iterator object creation on every dictionary conversion. Replace object spread (`{ ...opts, context }`) with explicit property assignment when passing opts to member converters. This avoids allocating a full spread copy per dictionary member. Signed-off-by: Filip Skokan --- lib/internal/crypto/webidl.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 4f371955a73bfd..884ce246f7723d 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -293,7 +293,8 @@ function createDictionaryConverter(name, dictionaries) { return idlDict; } - for (const member of new SafeArrayIterator(allMembers)) { + for (let i = 0; i < allMembers.length; i++) { + const member = allMembers[i]; const key = member.key; let esMemberValue; @@ -309,7 +310,7 @@ function createDictionaryConverter(name, dictionaries) { }`; const idlMemberValue = member.converter(esMemberValue, { __proto__: null, - ...opts, + prefix: opts.prefix, context, }); member.validator?.(idlMemberValue, esDict); @@ -317,7 +318,7 @@ function createDictionaryConverter(name, dictionaries) { } else if (member.required) { throw makeException( `can not be converted to '${name}' because '${key}' is required in '${name}'.`, - { __proto__: null, ...opts, code: 'ERR_MISSING_OPTION' }); + { __proto__: null, prefix: opts.prefix, context: opts.context, code: 'ERR_MISSING_OPTION' }); } }