Skip to content

Commit 5b7ec21

Browse files
hckhanhCopilot
andauthored
Optimize path, join, and removeNullOrUndef functions for performance and clarity, introduce regex pre-compilation, and improve path parameter validation. (#82)
* Optimize `path` function to handle templates without parameters more efficiently. * Optimize `join` function for common boundary scenarios and remove unused fast path logic in `path`. * Optimize `removeNullOrUndef` to improve performance by avoiding unnecessary object allocation and using direct property iteration. * Refactor `removeNullOrUndef` to simplify logic and avoid unnecessary null check flag. * Optimize `path` function by pre-compiling regex to avoid recompilation overhead. * Improve validation for path parameters by adding empty string check and clarifying allowed types. * Update Changesets config to ignore "web" package and add initial release note template * Add comprehensive changeset documentation for performance optimizations (#84) * Initial plan * Update changeset with comprehensive PR analysis and detailed optimization descriptions Co-authored-by: hckhanh <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hckhanh <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 5a1fe77 commit 5b7ec21

File tree

3 files changed

+78
-10
lines changed

3 files changed

+78
-10
lines changed

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"access": "restricted",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": []
10+
"ignore": ["web"]
1111
}

.changeset/petite-pumas-flash.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"fast-url": patch
3+
---
4+
5+
Performance optimizations for path and parameter handling utilities in `src/index.ts`. This release focuses on reducing regex recompilation overhead, optimizing string manipulation for path joining, and improving parameter filtering performance.
6+
7+
**Performance Optimizations:**
8+
9+
- **Pre-compiled regex**: Extracted the path parameter regex (`PATH_PARAM_REGEX`) to module scope to avoid recompiling it on every `path()` call, improving efficiency for path template processing.
10+
- **Optimized `join()` function**: Rewrote to use direct string indexing (`part1[len1 - 1]`, `part2[0]`) instead of `endsWith`/`startsWith` methods, added fast paths for empty strings, and optimized for the most common URL joining scenario where both parts have separators. This reduces unnecessary string slicing and improves speed.
11+
- **Optimized `removeNullOrUndef()` function**: Improved performance by checking for null/undefined values before allocating a new object, and using direct property iteration instead of `Object.entries`/`Object.fromEntries`. This results in faster execution and less memory usage, especially when no filtering is needed.
12+
13+
**Parameter Validation Improvements:**
14+
15+
- Enhanced `validatePathParam()` to check for empty strings in path parameters, ensuring that string path parameters cannot be empty or whitespace-only values.
16+
- Improved code readability by adding blank lines between logical blocks in validation logic.

src/index.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,28 +154,38 @@ export function subst(template: string, params: ParamMap): string {
154154
return renderedPath
155155
}
156156

157+
// Pre-compile regex for better performance - avoids recompilation overhead on each call
158+
const PATH_PARAM_REGEX = /:[_A-Za-z]+\w*/g
159+
157160
function path(template: string, params: ParamMap) {
158161
const remainingParams = { ...params }
159-
const renderedPath = template.replace(/:[_A-Za-z]+\w*/g, (p) => {
162+
163+
const renderedPath = template.replace(PATH_PARAM_REGEX, (p) => {
160164
const key = p.slice(1)
165+
161166
validatePathParam(params, key)
167+
162168
delete remainingParams[key]
163169
return encodeURIComponent(params[key] as string | number | boolean)
164170
})
171+
165172
return { renderedPath, remainingParams }
166173
}
167174

168175
function validatePathParam(params: ParamMap, key: string) {
169176
if (!Object.hasOwn(params, key)) {
170177
throw new Error(`Missing value for path parameter ${key}.`)
171178
}
179+
172180
const type = typeof params[key]
181+
173182
if (type !== 'boolean' && type !== 'string' && type !== 'number') {
174183
throw new TypeError(
175184
`Path parameter ${key} cannot be of type ${type}. ` +
176185
'Allowed types are: boolean, string, number.',
177186
)
178187
}
188+
179189
if (type === 'string' && (params[key] as string).trim() === '') {
180190
throw new Error(`Path parameter ${key} cannot be an empty string.`)
181191
}
@@ -199,11 +209,35 @@ function validatePathParam(params: ParamMap, key: string) {
199209
* ```
200210
*/
201211
export function join(part1: string, separator: string, part2: string): string {
202-
const p1 = part1.endsWith(separator)
203-
? part1.slice(0, -separator.length)
204-
: part1
205-
const p2 = part2.startsWith(separator) ? part2.slice(separator.length) : part2
206-
return !p1 || !p2 ? p1 + p2 : p1 + separator + p2
212+
const len1 = part1.length
213+
const len2 = part2.length
214+
215+
// Fast path: handle empty parts
216+
if (len1 === 0) {
217+
return len2 > 0 && part2[0] === separator ? part2.slice(1) : part2
218+
}
219+
220+
if (len2 === 0) {
221+
return part1[len1 - 1] === separator ? part1.slice(0, -1) : part1
222+
}
223+
224+
// Check boundaries using direct character access (faster than endsWith/startsWith)
225+
const p1EndsWithSep = part1[len1 - 1] === separator
226+
const p2StartsWithSep = part2[0] === separator
227+
228+
// Optimize for the common case where no trimming is needed
229+
if (!p1EndsWithSep && !p2StartsWithSep) {
230+
return part1 + separator + part2
231+
}
232+
233+
// Optimized: When both have separator, just remove from part2 (avoids slicing part1)
234+
// This is the most common case for URL building: "http://example.com/" + "/path"
235+
if (p1EndsWithSep && p2StartsWithSep) {
236+
return part1 + part2.slice(1)
237+
}
238+
239+
// One has separator, one doesn't - just concatenate
240+
return part1 + part2
207241
}
208242

209243
/**
@@ -220,7 +254,25 @@ export function join(part1: string, separator: string, part2: string): string {
220254
* ```
221255
*/
222256
function removeNullOrUndef<P extends ParamMap>(params: P) {
223-
return Object.fromEntries(
224-
Object.entries(params).filter(([, value]) => value != null),
225-
) as { [K in keyof P]: NonNullable<P[K]> }
257+
// Optimized: Direct property iteration is faster than Object.entries/fromEntries
258+
// Fast path: check if any null/undefined exists first
259+
for (const key in params) {
260+
if (Object.hasOwn(params, key) && params[key] == null) {
261+
// Build a new object only if needed
262+
const result: ParamMap = {}
263+
for (const key in params) {
264+
if (Object.hasOwn(params, key)) {
265+
const value = params[key]
266+
if (value != null) {
267+
result[key] = value
268+
}
269+
}
270+
}
271+
272+
return result as { [K in keyof P]: NonNullable<P[K]> }
273+
}
274+
}
275+
276+
// If no null/undefined values, return as-is (avoid object allocation)
277+
return params as { [K in keyof P]: NonNullable<P[K]> }
226278
}

0 commit comments

Comments
 (0)