Repository navigation
perf: escape strings with JSON.stringify instead of a per-character loop - #217
Merged
elliott-with-the-longest-name-on-github merged 2 commits intoOct 7, 2026
Conversation
Strings that contain anything to escape walked every character in JS, appending a slice per escaped character. Content with many quotes (JSON embedded in a string, source code) hit that path hard: ~12 ms/MB. JSON.stringify already produces every escape stringify_string emits — quotes, backslashes, control characters, and \udXXX for unpaired surrogates — except for `<` and the U+2028/U+2029 separators, which it leaves raw. Use it and fix those three up in one regex pass. Output is byte-identical to the previous implementation; the existing escape and surrogate tests pass unchanged. 2-3x faster on multi-MB JSON-shaped strings.
🦋 Changeset detectedLatest commit: 49a761f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
http-samc
commented
Oct 6, 2026
http-samc
left a comment
Contributor
Author
There was a problem hiding this comment.
Reviewed against main. I found nothing wrong with this change: the output is provably identical to the previous implementation, the perf numbers reproduce, and the diff is a clean simplification. Comments inline are a style nit and a small test-coverage suggestion.
Verified locally
- Equivalence: old vs new
stringify_stringcompared over 418,318 inputs (every BMP code unit in three contexts, all surrogate pairings, an astral sweep, hand-picked specials including the empty string, and 200k random strings over"/\/</controls/U+2028/U+2029/lone and paired surrogates/é///U+007F): 0 mismatches, all outputsevalback to the input. pnpm test: 1307/1307 pass.pnpm exec prettier --check src .changeset: clean.- Perf, JSON-shaped strings on Node 24 (old → new): 0.3 MB 8.2 → 1.9 ms; 7.2 MB 345 → 54 ms; 34.6 MB 1731 → 243 ms. A 7 MB plain string with a single trailing
<went 94 → 47 ms, and a 7 MB string with a single lone surrogate 110 → 70 ms. The fast path (emoji, nothing to escape) is unchanged (28 vs 30 ms, noise). So the table in the description is, if anything, conservative on this machine. - Changeset present (
devaluepatch), which matches how the previousperf:entries inCHANGELOG.mdwere released.
Nits
- Title/changeset prefix: this is a performance refactor with no behavior change, and the repo's recent history uses
perf:for exactly this kind of commit (perf: cheaper stringify for plain data and strings (#190)).fix:suggests a correctness bug was present. Considerperf:for both the PR title and the changeset line so the changelog groups it with its siblings. - The comment block above
json_leaves_rawis accurate (JSON.stringify's handling of unpaired surrogates has been well-formed since ES2019). The parenthetical about older engines rejecting U+2028/U+2029 in string literals is historical since ES2019 as well; still a fine reason to keep escaping them for compatibility, just noting it's not a current-engine constraint.
elliott-with-the-longest-name-on-github
approved these changes
Oct 7, 2026
Contributor
|
Thank you! |
elliott-with-the-longest-name-on-github
merged commit Oct 7, 2026
5cd712a
into
sveltejs:main
5 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
stringify_stringhas a fast path for strings with nothing to escape, but anything that needs even one escape falls into a JS loop over every character, appending a slice per escaped character. Content with many quotes (JSON embedded in a string, source code, log lines) hits that path hard: about 12 ms/MB locally.JSON.stringifyalready produces every escapestringify_stringemits —",\, control characters as\n/\t/\u001fetc., and lowercase\udXXXfor unpaired surrogates — except for<and the U+2028/U+2029 separators, which it leaves raw. This change uses it and fixes those three up in a single regex pass over a small lookup table. Output is byte-identical to the previous implementation (verified by comparing old and new over every BMP code unit in several contexts, all surrogate pairings, and random strings over the escape alphabet: 0 mismatches), so the existing escape, XSS and surrogate tests pass unchanged. Two tests are added: one for the three post-pass characters mixed with JSON escapes and an unpaired surrogate, and one asserting that charactersJSON.stringifyleaves raw (/, DEL, non-ASCII,>) stay raw on the escaping path.On JSON-shaped strings:
Net −38 lines (drops
get_escaped_charand the loop).