From f502226d3f704d0793d4175275b499aff21dd4f4 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 29 Oct 2026 10:35:00 -0700 Subject: [PATCH] Improve perf and package size --- package-lock.json | 94 ++++++++++++++++++++++++++++++++++++---------- package.json | 12 +++++- src/index.bench.ts | 23 ++++++++++++ src/index.ts | 16 ++------ tsconfig.json | 2 +- 5 files changed, 113 insertions(+), 34 deletions(-) create mode 100644 src/index.bench.ts diff --git a/package-lock.json b/package-lock.json index b3f093d..7c08b7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "license": "MIT", "devDependencies": { "@borderless/ts-scripts": "^0.15.0", + "@size-limit/preset-small-lib": "^14.3.0", "@types/node": "^26.6.5", "@vitest/coverage-v8": "^5.0.5", + "size-limit": "^14.3.0", "typescript": "^7.0.2", "vitest": "^5.0.5" }, @@ -400,7 +402,6 @@ "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/oxc-project" } @@ -429,7 +430,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -447,7 +447,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -465,7 +464,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -483,7 +481,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -501,7 +498,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -519,7 +515,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -540,7 +535,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -561,7 +555,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -582,7 +575,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -603,7 +595,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -624,7 +615,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -645,7 +635,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -663,7 +652,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -681,7 +669,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -699,7 +686,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -709,8 +695,51 @@ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, + "license": "MIT" + }, + "node_modules/@size-limit/file": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@size-limit/file/-/file-14.3.0.tgz", + "integrity": "sha512-GWKOi0rorXPbYmwbT926Z91t28rg5gd5hv8ywY1FNcaYf8NbibgK5K5YabYbLkV6KpGwuuixBRtIzl3ZpPzUfw==", + "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": "^22.19.0 || ^24.5.0 || >=26.0.0" + }, + "peerDependencies": { + "size-limit": "14.3.0" + } + }, + "node_modules/@size-limit/preset-small-lib": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@size-limit/preset-small-lib/-/preset-small-lib-14.3.0.tgz", + "integrity": "sha512-nvCyY+o26aOIdZxm81nlebE9+9lVf9UMKi19hyjE5DjO5kSaOTtnrZSeHzV9JTN5djt/hqGserT1mecoGXLTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@size-limit/file": "14.3.0", + "@size-limit/rolldown": "14.3.0", + "size-limit": "14.3.0" + }, + "peerDependencies": { + "size-limit": "14.3.0" + } + }, + "node_modules/@size-limit/rolldown": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@size-limit/rolldown/-/rolldown-14.3.0.tgz", + "integrity": "sha512-u4qTH1R5gAonGYYEBLb4UHOUpWhiwVOATpV/wvKCzGTcrwziYdWzvK2S4PuUKGSj9C8/NU2d1KqhFJZy5Mw3XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rolldown": "^1.2.15" + }, + "engines": { + "node": "^22.19.0 || ^24.5.0 || >=26.0.0" + }, + "peerDependencies": { + "size-limit": "14.3.0" + } }, "node_modules/@types/chai": { "version": "5.2.3", @@ -2609,6 +2638,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -2688,8 +2727,7 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.2", @@ -2912,7 +2950,6 @@ "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" @@ -2941,6 +2978,23 @@ "@rolldown/binding-win32-x64-msvc": "1.2.16" } }, + "node_modules/size-limit": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-14.3.0.tgz", + "integrity": "sha512-SoT7DtEYQ9AkOIyrNmmHu8IyJyA3P/Q6EUVjUUinT0iBuaI6IOpWxwyI+teUxg8WdLwcTwbu9moKvw/LGlCE7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.4", + "nanospinner": "^1.2.2" + }, + "bin": { + "size-limit": "bin.js" + }, + "engines": { + "node": "^22.19.0 || ^24.5.0 || >=26.0.0" + } + }, "node_modules/sort-object-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.1.0.tgz", diff --git a/package.json b/package.json index ad49019..67dbff5 100644 --- a/package.json +++ b/package.json @@ -24,17 +24,21 @@ "dist/" ], "scripts": { + "bench": "vitest bench", "build": "ts-scripts build", "format": "ts-scripts format", "lint": "ts-scripts check", "prepare": "ts-scripts install && npm run build", + "size": "size-limit", "specs": "ts-scripts specs", - "test": "ts-scripts test" + "test": "ts-scripts test && npm run size" }, "devDependencies": { "@borderless/ts-scripts": "^0.15.0", + "@size-limit/preset-small-lib": "^14.3.0", "@types/node": "^26.6.5", "@vitest/coverage-v8": "^5.0.5", + "size-limit": "^14.3.0", "typescript": "^7.0.2", "vitest": "^5.0.5" }, @@ -44,6 +48,12 @@ "publishConfig": { "access": "public" }, + "size-limit": [ + { + "path": "dist/index.js", + "limit": "250 B" + } + ], "ts-scripts": { "dist": [ "dist" diff --git a/src/index.bench.ts b/src/index.bench.ts new file mode 100644 index 0000000..736f7fb --- /dev/null +++ b/src/index.bench.ts @@ -0,0 +1,23 @@ +import { describe, test } from "vitest"; +import { encodeUrl } from "./index.js"; + +const _encodeUrl = encodeUrl; + +describe("encodeUrl", () => { + const cases = { + "plain URL": "https://example.com/foo/bar?hello=world#section", + "already encoded": "https://example.com/foo%20bar?q=caf%C3%A9", + spaces: "https://example.com/foo bar?q=hello world", + Unicode: "https://example.com/café/日本語", + emoji: "https://example.com/👻😀🚀", + "unpaired surrogates": "https://example.com/\uD800foo\uDC00", + "invalid escapes": "https://example.com/%foo%bar%zap%", + "long plain URL": `https://example.com/${"foo/bar/".repeat(500)}`, + }; + + for (const [name, url] of Object.entries(cases)) { + test(name, async ({ bench }) => { + await bench(name, () => _encodeUrl(url)).run(); + }); + } +}); diff --git a/src/index.ts b/src/index.ts index 46cac2c..9cb730f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,13 +10,7 @@ * @private */ const ENCODE_CHARS_REGEXP = - /(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g; - -/** - * RegExp to match surrogate pairs and unpaired surrogates. - * @private - */ -const SURROGATE_REGEXP = /([\uD800-\uDBFF][\uDC00-\uDFFF])|[\uD800-\uDFFF]/g; + /(?:[\x00-\x20"<>`{}\x7F-\uFFFF]|%(?:[0-9A-Fa-f]?[^0-9A-Fa-f]|$))+/g; /** * Encode a URL to a percent-encoded form, excluding already-encoded sequences. @@ -36,9 +30,7 @@ const SURROGATE_REGEXP = /([\uD800-\uDBFF][\uDC00-\uDFFF])|[\uD800-\uDFFF]/g; * @public */ export function encodeUrl(url: string): string { - return url - .replace(SURROGATE_REGEXP, function (_: string, pair: string | undefined) { - return pair || "\uFFFD"; - }) - .replace(ENCODE_CHARS_REGEXP, encodeURI); + const index = url.search(ENCODE_CHARS_REGEXP); + if (index === -1) return url; + return url.toWellFormed().replace(ENCODE_CHARS_REGEXP, encodeURI); } diff --git a/tsconfig.json b/tsconfig.json index e89a674..053abc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@borderless/ts-scripts/configs/tsconfig.json", "compilerOptions": { "target": "ES2023", - "lib": ["ES2023"], + "lib": ["ES2023", "ES2024.String"], "rootDir": "src", "outDir": "dist", "module": "NodeNext",