Skip to content

perf: add early exit in valueSlice for empty ranges#259

Closed
Ayoub-Mabrouk wants to merge 1 commit intojshttp:masterfrom
Ayoub-Mabrouk:optimize/value-slice-early-exit
Closed

perf: add early exit in valueSlice for empty ranges#259
Ayoub-Mabrouk wants to merge 1 commit intojshttp:masterfrom
Ayoub-Mabrouk:optimize/value-slice-early-exit

Conversation

@Ayoub-Mabrouk
Copy link

add early return when min >= max to avoid unnecessary processing for empty string ranges.
This handles edge cases like empty cookie values, empty string inputs, empty attribute values, and consecutive semicolons.
This optimization avoids character code checks and loop iterations when the slice range is empty or invalid.

@blakeembrey
Copy link
Member

blakeembrey commented Jan 26, 2026

I don't think this provides any meaningful change in performance. I included the benchmarks below but it's inconclusive. We'd need to do a more specific benchmark to isolate the changes.

At least intuitively, this adds an extra check to every parse when the majority won't be empty. It seems like a de-optimization for the main use-case, and optimizing for an edge case. That said, it can probably be improved but it should be included with benchmarks that show an improvement in the majority of cases.

The things you describe as improving are fairly rare, only empty cookie values are likely encountered in the real world and those are going to be massively outnumbered but the amount of cookies that include values.

Main:

 ✓ src/parse-cookie.bench.ts > cookie.parseCookie 12382ms
     name                    hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · empty        26,822,131.73  0.0000  0.1825  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.13%  13411066
   · simple       10,558,365.22  0.0000  0.0437  0.0001  0.0001  0.0001  0.0001  0.0002  ±0.07%   5279183
   · decode        4,510,222.17  0.0001  0.3647  0.0002  0.0002  0.0003  0.0003  0.0004  ±0.82%   2255112
   · unquote       9,958,343.72  0.0000  0.4741  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.60%   4979172
   · duplicates    2,813,956.95  0.0002  0.0665  0.0004  0.0004  0.0004  0.0005  0.0005  ±0.07%   1406979
   · 10 cookies      932,535.65  0.0009  0.3794  0.0011  0.0011  0.0012  0.0013  0.0017  ±0.51%    466268
   · 100 cookies      74,576.65  0.0123  0.7195  0.0134  0.0131  0.0171  0.0204  0.2326  ±0.88%     37289

 ✓ src/parse-cookie.bench.ts > parse top-sites 19371ms
     name                                 hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · parse accounts.google.com  7,819,628.78  0.0000  0.5068  0.0001  0.0001  0.0002  0.0002  0.0003  ±1.31%  3909815
   · parse apple.com            9,476,345.58  0.0000  0.0837  0.0001  0.0001  0.0001  0.0002  0.0002  ±0.07%  4738173
   · parse cloudflare.com       1,592,510.24  0.0005  0.2092  0.0006  0.0006  0.0007  0.0008  0.0011  ±0.13%   796256
   · parse docs.google.com      8,111,597.73  0.0000  0.2308  0.0001  0.0001  0.0002  0.0002  0.0003  ±0.71%  4055800
   · parse drive.google.com     8,164,042.48  0.0000  0.2663  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.76%  4082022
   · parse linkedin.com         1,900,834.45  0.0004  0.2502  0.0005  0.0005  0.0006  0.0007  0.0008  ±0.22%   950418
   · parse play.google.com      7,644,590.62  0.0000  1.9103  0.0001  0.0001  0.0003  0.0004  0.0004  ±1.10%  3822296
   · parse policies.google.com  7,763,371.21  0.0000  8.5856  0.0001  0.0001  0.0002  0.0002  0.0003  ±3.38%  3881686
   · parse support.google.com   4,610,285.62  0.0001  2.9541  0.0002  0.0002  0.0003  0.0003  0.0005  ±1.42%  2305143
   · parse wa.me                8,467,931.15  0.0000  2.7539  0.0001  0.0001  0.0002  0.0002  0.0003  ±1.12%  4233966
   · parse whatsapp.com         3,280,267.66  0.0002  0.3073  0.0003  0.0003  0.0004  0.0004  0.0006  ±0.33%  1640134
   · parse www.google.com       4,792,690.20  0.0001  0.2195  0.0002  0.0002  0.0003  0.0003  0.0005  ±0.58%  2396346
   · parse youtu.be             1,119,391.55  0.0008  0.2062  0.0009  0.0009  0.0010  0.0010  0.0014  ±0.11%   559696
   · parse youtube.com          1,109,935.56  0.0007  0.3140  0.0009  0.0009  0.0010  0.0010  0.0014  ±0.45%   554968

 ✓ src/parse-set-cookie.bench.ts > parse top-sites 11771ms
     name                                 hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · parse accounts.google.com  2,036,144.43  0.0004  0.4947  0.0005  0.0005  0.0006  0.0006  0.0015  ±0.41%  1018073
   · parse apple.com            6,204,210.21  0.0000  0.0450  0.0002  0.0002  0.0002  0.0002  0.0003  ±0.06%  3102106
   · parse cloudflare.com         328,824.97  0.0028  0.2126  0.0030  0.0030  0.0035  0.0043  0.0126  ±0.41%   164413
   · parse docs.google.com      1,973,791.39  0.0004  0.2121  0.0005  0.0005  0.0006  0.0006  0.0023  ±0.46%   987093
   · parse drive.google.com     1,989,401.92  0.0004  0.2004  0.0005  0.0005  0.0006  0.0006  0.0009  ±0.44%   994701
   · parse linkedin.com           378,624.31  0.0025  0.2105  0.0026  0.0026  0.0030  0.0031  0.0058  ±0.22%   189313
   · parse play.google.com      2,178,853.41  0.0003  0.2548  0.0005  0.0005  0.0005  0.0006  0.0009  ±0.36%  1089427
   · parse policies.google.com  2,173,837.57  0.0003  0.2344  0.0005  0.0005  0.0005  0.0006  0.0009  ±0.37%  1086919
   · parse support.google.com   1,099,959.39  0.0007  0.3007  0.0009  0.0009  0.0010  0.0011  0.0020  ±0.50%   549980
   · parse wa.me                1,854,744.44  0.0004  0.5137  0.0005  0.0005  0.0007  0.0007  0.0010  ±0.50%   927373
   · parse whatsapp.com           695,211.31  0.0013  0.1843  0.0014  0.0014  0.0016  0.0017  0.0033  ±0.30%   347606
   · parse www.google.com         964,180.32  0.0009  1.8574  0.0010  0.0010  0.0012  0.0013  0.0037  ±0.81%   482091
   · parse youtu.be               280,234.69  0.0033  1.4187  0.0036  0.0035  0.0040  0.0045  0.0095  ±0.63%   140118
   · parse youtube.com            281,681.65  0.0033  0.1942  0.0036  0.0035  0.0039  0.0042  0.0082  ±0.30%   140841

PR:

 ✓ src/parse-cookie.bench.ts > cookie.parseCookie 12375ms
     name                    hz     min     max    mean     p75     p99    p995    p999     rme   samples
   · empty        26,579,734.07  0.0000  0.1364  0.0000  0.0000  0.0000  0.0000  0.0001  ±0.15%  13289869
   · simple       10,667,071.36  0.0000  0.0589  0.0001  0.0001  0.0001  0.0002  0.0002  ±0.08%   5333536
   · decode        4,548,135.10  0.0001  0.3423  0.0002  0.0002  0.0003  0.0003  0.0004  ±0.83%   2274068
   · unquote       9,850,735.99  0.0000  0.3116  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.54%   4925369
   · duplicates    2,679,765.62  0.0002  0.0440  0.0004  0.0004  0.0005  0.0005  0.0005  ±0.07%   1339883
   · 10 cookies      928,374.30  0.0009  0.3788  0.0011  0.0011  0.0012  0.0013  0.0031  ±0.42%    464188
   · 100 cookies      76,444.54  0.0122  0.4226  0.0131  0.0128  0.0156  0.0176  0.2486  ±0.81%     38223

 ✓ src/parse-cookie.bench.ts > parse top-sites 19559ms
     name                                 hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · parse accounts.google.com  7,688,466.94  0.0000  0.4392  0.0001  0.0001  0.0002  0.0002  0.0003  ±1.28%  3844234
   · parse apple.com            9,614,078.77  0.0000  0.0564  0.0001  0.0001  0.0001  0.0002  0.0002  ±0.09%  4807040
   · parse cloudflare.com       1,583,352.32  0.0005  0.2350  0.0006  0.0006  0.0007  0.0008  0.0013  ±0.16%   791677
   · parse docs.google.com      7,991,367.41  0.0000  0.2248  0.0001  0.0001  0.0002  0.0002  0.0003  ±0.66%  3995684
   · parse drive.google.com     7,937,225.98  0.0000  0.2433  0.0001  0.0001  0.0002  0.0002  0.0003  ±0.76%  3968613
   · parse linkedin.com         1,893,172.95  0.0004  0.2035  0.0005  0.0005  0.0006  0.0007  0.0009  ±0.21%   946587
   · parse play.google.com      8,827,109.63  0.0000  0.2195  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.32%  4413555
   · parse policies.google.com  8,687,284.89  0.0000  2.0260  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.84%  4343643
   · parse support.google.com   4,985,379.86  0.0001  0.2455  0.0002  0.0002  0.0003  0.0003  0.0004  ±0.54%  2492690
   · parse wa.me                8,247,776.88  0.0000  0.2183  0.0001  0.0001  0.0001  0.0002  0.0003  ±0.43%  4123889
   · parse whatsapp.com         3,451,897.81  0.0002  9.4494  0.0003  0.0003  0.0004  0.0004  0.0007  ±3.71%  1725949
   · parse www.google.com       5,100,781.63  0.0001  0.3259  0.0002  0.0002  0.0002  0.0003  0.0005  ±0.52%  2550391
   · parse youtu.be             1,122,611.57  0.0007  0.1957  0.0009  0.0009  0.0010  0.0010  0.0013  ±0.34%   561306
   · parse youtube.com          1,122,509.49  0.0007  0.2585  0.0009  0.0009  0.0011  0.0011  0.0026  ±0.12%   561255

 ✓ src/parse-set-cookie.bench.ts > parse top-sites 11795ms
     name                                 hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · parse accounts.google.com  2,042,078.04  0.0004  0.4755  0.0005  0.0005  0.0006  0.0006  0.0015  ±0.40%  1021040
   · parse apple.com            6,116,261.31  0.0000  0.0359  0.0002  0.0002  0.0002  0.0002  0.0003  ±0.05%  3058131
   · parse cloudflare.com         338,732.23  0.0027  0.2120  0.0030  0.0029  0.0033  0.0035  0.0081  ±0.37%   169367
   · parse docs.google.com      1,978,404.06  0.0004  0.2743  0.0005  0.0005  0.0006  0.0006  0.0010  ±0.44%   989203
   · parse drive.google.com     1,972,868.53  0.0004  0.1946  0.0005  0.0005  0.0006  0.0006  0.0010  ±0.45%   986435
   · parse linkedin.com           377,573.58  0.0025  0.1938  0.0026  0.0027  0.0030  0.0031  0.0051  ±0.22%   188787
   · parse play.google.com      2,157,709.86  0.0003  0.2272  0.0005  0.0005  0.0005  0.0006  0.0008  ±0.37%  1078855
   · parse policies.google.com  2,143,537.76  0.0003  0.2127  0.0005  0.0005  0.0005  0.0006  0.0010  ±0.38%  1071769
   · parse support.google.com   1,087,225.41  0.0008  0.2535  0.0009  0.0009  0.0010  0.0011  0.0034  ±0.51%   543613
   · parse wa.me                1,835,669.58  0.0004  0.5899  0.0005  0.0005  0.0007  0.0007  0.0012  ±0.55%   917835
   · parse whatsapp.com           671,624.34  0.0013  0.1987  0.0015  0.0015  0.0017  0.0018  0.0060  ±0.32%   335813
   · parse www.google.com         929,454.66  0.0009  0.2259  0.0011  0.0010  0.0013  0.0015  0.0090  ±0.37%   464728
   · parse youtu.be               277,734.87  0.0033  0.1881  0.0036  0.0036  0.0040  0.0050  0.0130  ±0.32%   138868
   · parse youtube.com            276,436.55  0.0034  0.2130  0.0036  0.0036  0.0040  0.0044  0.0133  ±0.34%   138219

@blakeembrey
Copy link
Member

I tried other variations on this idea and haven't come up with any performance improvement. For example, slicing an empty string vs '' doesn't seem to be any different. Nor does making both a while instead of do..while. In fact, checking character codes seems to be incredibly fast, maybe even faster than relying on the numeric check of the while loop.

@blakeembrey
Copy link
Member

I tried taking this change a bit more seriously and wrote up a benchmark but it's inconclusive too:

import { describe, bench, expect } from "vitest";

const CASES = [
  {
    text: "test=example",
    start: 5,
    end: 12,
    expected: "example",
  },
  {
    text: "test=",
    start: 5,
    end: 5,
    expected: "",
  },
  {
    text: "test= example ",
    start: 5,
    end: 14,
    expected: "example",
  },
  {
    text: "test=example ",
    start: 5,
    end: 13,
    expected: "example",
  },
  {
    text: "test= example",
    start: 5,
    end: 13,
    expected: "example",
  },
];

function valueSliceDoThenWhileEmptyString(str: string, min: number, max: number) {
  let start = min;
  let end = max;

  do {
    const code = str.charCodeAt(start);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
  } while (++start < end);

  while (end > start) {
    const code = str.charCodeAt(end - 1);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
    end--;
  }

  return start === end ? "" : str.slice(start, end);
}

function valueSliceDoThenWhile(str: string, min: number, max: number) {
  let start = min;
  let end = max;

  do {
    const code = str.charCodeAt(start);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
  } while (++start < end);

  while (end > start) {
    const code = str.charCodeAt(end - 1);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
    end--;
  }

  return str.slice(start, end);
}

function valueSliceTwoWhile(str: string, min: number, max: number) {
  let start = min;
  let end = max;

  while (start < end) {
    const code = str.charCodeAt(start);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
    start++;
  }

  while (end > start) {
    const code = str.charCodeAt(end - 1);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
    end--;
  }

  return str.slice(start, end);
}

function valueSliceTwoDoWhile(str: string, min: number, max: number) {
  let start = min;
  let end = max;

  do {
    const code = str.charCodeAt(start);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
  } while (++start < end);

  do {
    const code = str.charCodeAt(end - 1);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
  } while (--end > start);

  return str.slice(start, end);
}

function valueSliceDoThenWhileExitEarly(str: string, min: number, max: number) {
  if (min === max) return "";
  let start = min;
  let end = max;

  do {
    const code = str.charCodeAt(start);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
  } while (++start < end);

  while (end > start) {
    const code = str.charCodeAt(end - 1);
    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) break;
    end--;
  }

  return str.slice(start, end);
}

function valueSliceThenTrim(str: string, min: number, max: number) {
  return str.slice(min, max).trim();
}

describe.each(CASES)("valueSlice $text", ({ text, start, end, expected }) => {
  bench("do...while then while", () => {
    expect(valueSliceDoThenWhile(text, start, end)).toBe(expected);
  });

  bench("two while", () => {
    expect(valueSliceTwoWhile(text, start, end)).toBe(expected);
  });

  bench("two do...while", () => {
    expect(valueSliceTwoDoWhile(text, start, end)).toBe(expected);
  });

  bench("do...while then while (exit early)", () => {
    expect(valueSliceDoThenWhileExitEarly(text, start, end)).toBe(expected);
  });

  bench("do...while then while (empty string check)", () => {
    expect(valueSliceDoThenWhileEmptyString(text, start, end)).toBe(expected);
  });

  bench("slice then trim", () => {
    expect(valueSliceThenTrim(text, start, end)).toBe(expected);
  });
});

I think the only takeaway from this is that .slice().trim() is comparable and maybe worth using instead to save bytes. It'd need to be tested in more JS engines though.

@blakeembrey
Copy link
Member

blakeembrey commented Jan 26, 2026

I opened a PR with everything I did to review this one here: #261. This took me a lot of time and I don't think I'll be doing it again for a small tweak like this. In the future, it'd be greatly appreciated if you have a performance optimization to include some level of testing and benchmarking to prove that it hasn't impacted existing code and has actually improved things, as well as other possibilities you've tested (basically what I did above and in the PR).

Once this code combines with everything else and real world tests I think it ends up being basically non-existent still, though it does look like maybe a 10% bump is achieved in the empty string case while the negative impact to other methods was less than I expected.

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