Skip to content

Perf optimize cookie value validation#285

Open
blakeembrey wants to merge 1 commit into
masterfrom
be/skip-validation-default-encoder
Open

Perf optimize cookie value validation#285
blakeembrey wants to merge 1 commit into
masterfrom
be/skip-validation-default-encoder

Conversation

@blakeembrey

Copy link
Copy Markdown
Member

Tweak encoder regex to be a negative class match (benchmark is slightly faster, but marginal, could undo if someone really disagrees) and skips validation of the value entirely when using the default encoder since it's already known valid.

@blakeembrey
blakeembrey force-pushed the be/skip-validation-default-encoder branch from a7aa0f0 to 2824b73 Compare July 8, 2026 18:36
@TeeAaTeeUu

TeeAaTeeUu commented Jul 14, 2026

Copy link
Copy Markdown

If more code is OK, an extra ~6% perf-increase can be achieved by replacing regex with a for-loop and individually checking charCode:

function defaultEncode(str: string): string  {
  for (let i = 0, c = 0, len = str.length; i < len; i++) {
    c = str.charCodeAt(i);
    // \u0000-\u0020 (<33), \u007F-\uFFFF (>126), "(34), %(37), ,(44), ;(59), \(92)
    if (c < 33 || c > 126 || c === 34 || c === 37 || c === 44 || c === 59 || c === 92)
      return encodeURIComponent(str);
  }
  return str;
}

Runtime: node 26.4.0 (arm64-darwin)

case before hz after hz Speedup
empty 25.380.759,34 25.288.506,03 1,00x
simple 12.159.813,17 13.313.107,04 1,09x
rfc cookie-octets 11.743.931,74 11.103.105,49 0,95x
encode 5.753.640,44 6.171.864,26 1,07x
undefined values 5.754.738,96 6.303.222,05 1,10x
mixed encode 3.422.381,20 3.702.327,76 1,08x
10 cookies 1.567.005,95 1.722.698,91 1,10x
100 cookies 134.028,73 141.792,50 1,06x

I think with these we are getting into the "not so relevant speedup" territory, meaning code-readability should be prioritised instead. If you read and understand regex more easily, then that is preferable than above for-loop!

@TeeAaTeeUu

Copy link
Copy Markdown

And for completion, here also the other regexes in for-loop form, gaining ~10% more (at least with the current bench, we might want to test with longer strings):

function validCookieName(str: string): boolean  {
  for (let i = 0, c = 0, len = str.length; i < len; i++) {
    c = str.charCodeAt(i);
    // ;(59) =(61)
    if (c < 33 || c > 126 || c === 59 || c === 61)
      return false;
  }
  return true;
}

function validCookieValue(str: string): boolean  {
  for (let i = 0, c = 0, len = str.length; i < len; i++) {
    c = str.charCodeAt(i);
    // ;(59) 
    if (c < 33 || c > 126 || c === 59) return false;
  }
  return true;
}

function validCookiePath(str: string): boolean  {
  for (let i = 0, c = 0, len = str.length; i < len; i++) {
    c = str.charCodeAt(i);
    // ;(59) <(60)
    if (c < 32 || c > 126 || c === 59 || c === 60) return false;
  }
  return true;
}

And in case wanted to add a bit more readability, we can generate the charCode values:

const EXLAMATION = '!'.charCodeAt(0)
const DOUBLEQUOTE = '"'.charCodeAt(0)
const PERCENT = '%'.charCodeAt(0)
const COMMA = ','.charCodeAt(0)
const SEMICOLON = ';'.charCodeAt(0)
const LESS_THAN = '<'.charCodeAt(0)
const EQUAL = '='.charCodeAt(0)
const BACKSLASH = '\\'.charCodeAt(0)
const TILDE = '~'.charCodeAt(0)

@TeeAaTeeUu

Copy link
Copy Markdown

Walking back on my suggestion...

For-loop perf improvement only happens when the invalid / encoding-required character is found within the first ~15 characters. By the time it's 512 chars, for-loop is twice as slow.

Meaning for-loop is still faster for longer strings, if for example ; semicolon is found early within the string.

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.

2 participants