Skip to content

Conversation

Llorx
Copy link

@Llorx Llorx commented Oct 3, 2025

I noticed that on long strings, escapeLiteral goes really slow. I created my own escapeLiteral and I decided to push it. I saw this issue when searching: #3194

On short strings, the performance loss is very low compared to the benefit on long strings. On short strings the original is only 1.17x faster, but on long strings the new one is 30x (!!) faster, so at the moment that the strings are a bit longer than 10 characters, the performance profit is going to kick in, and the performance loss on short strings is negligible.

Anyway, with this information I decided to add a quick check: If the string is shorter than 13 characters, use the original one, and if it is longer, use the optimized one. It has the performance profit in both situations:
image

EDIT: Had a typo in the benchmark, it is only 30x faster, not 55x.

Here the benchmark code:

const { IsoBench } = require("iso-bench");

function escapeLiteralOriginal(str) {
  let hasBackslash = false
  let escaped = "'"

  if (str == null) {
    return "''"
  }

  if (typeof str !== 'string') {
    return "''"
  }

  for (let i = 0; i < str.length; i++) {
    const c = str[i]
    if (c === "'") {
      escaped += c + c
    } else if (c === '\\') {
      escaped += c + c
      hasBackslash = true
    } else {
      escaped += c
    }
  }

  escaped += "'"

  if (hasBackslash === true) {
    escaped = ' E' + escaped
  }

  return escaped
}
const escapeLiteralNew = function (str) {
  if (typeof str !== 'string') {
    return "''"
  }
  let hasBackslash = false
  if (str.length < 13) {
    let escaped = "'"

    for (let i = 0; i < str.length; i++) {
      const c = str[i]
      if (c === "'") {
        escaped += c + c
      } else if (c === '\\') {
        escaped += c + c
        hasBackslash = true
      } else {
        escaped += c
      }
    }
  
    escaped += "'"
  
    if (hasBackslash === true) {
      escaped = ' E' + escaped
    }
    return escaped
  } else {
    let escaped = str
        .replace(/\\/g, () => {
            hasBackslash = true
            return '\\\\'
        })
        .replace(/'/g, "''")
    
    if (hasBackslash) {
        escaped = ` E'${escaped}'`
    } else {
        escaped = `'${escaped}'`
    }
    return escaped
  }
}
function newArray(size, backslash, quote, count = 100) {
    return new Array(count).fill(0).map((_, i) => {
        const str = "a".repeat(size);
        if (backslash) {
            const pos = i % size;
            str[pos] = "\\";
        }
        if (quote) {
            const pos = (i + 2) % size;
            str[pos] = "'";
        }
        return str;
    });
}
new IsoBench()
    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(10, false, false))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(10, false, false))
.endGroup("short string, backslash = false, quote = false")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(10, true, false))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(10, true, false))
.endGroup("short string, backslash = true, quote = false")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(10, false, true))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(10, false, true))
.endGroup("short string, backslash = false, quote = true")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(10, true, true))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(10, true, true))
.endGroup("short string, backslash = true, quote = true")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(1000, false, false))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(1000, false, false))
.endGroup("long string, backslash = false, quote = false")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(1000, true, false))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(1000, true, false))
.endGroup("long string, backslash = true, quote = false")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(1000, false, true))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(1000, false, true))
.endGroup("long string, backslash = false, quote = true")

    .add("escapeLiteralNew", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralNew(arr[i]);
        }
    }, () => newArray(1000, true, true))
    .add("escapeLiteralOriginal", (arr) => {
        for (let i = 0; i < arr.length; i++) {
            escapeLiteralOriginal(arr[i]);
        }
    }, () => newArray(1000, true, true))
.endGroup("long string, backslash = true, quote = true")
    .consoleLog()
    .run();

@Llorx
Copy link
Author

Llorx commented Oct 3, 2025

I don't understand the error the linter is returning

@Llorx Llorx changed the title Improve escapeIdentifier performance on long strings by 55x Improve escapeLiteral performance on long strings by 55x Oct 3, 2025
@Llorx Llorx changed the title Improve escapeLiteral performance on long strings by 55x Improve escapeLiteral performance on long strings by 30x Oct 3, 2025
@Llorx Llorx changed the title Improve escapeLiteral performance on long strings by 30x Improve escapeLiteral performance of long strings by 30x Oct 3, 2025
Copy link
Collaborator

@charmander charmander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m against the complexity of having a second path for short strings. (I don’t even think this function should have a second path for strings without backslashes, but that’d have to be a pg 9 change.) There are probably cleaner optimization options, since IIRC the original implementation here was just ported from C.

Also not sure how much performance matters past a certain point: as mentioned in #3194, people should pretty much always be using parameters instead of escapeLiteral anyway.

The linter is complaining about trailing spaces.

@charmander
Copy link
Collaborator

if (backslash) {
    const pos = i % size;
    str[pos] = "\\";
}
if (quote) {
    const pos = (i + 2) % size;
    str[pos] = "'";
}

str is a string (immutable), so this benchmark doesn’t actually test any quotes or backslashes.

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