Skip to content

Commit 1dbf8e3

Browse files
ngbrownpcattori
andauthored
Improve href() with a faster trim end splat (#14329)
* Faster trim end splat for href() * Add changeset * Remove link to benchmarks * minor refactors --------- Co-authored-by: Pedro Cattori <[email protected]>
1 parent 3df0dde commit 1dbf8e3

File tree

2 files changed

+27
-4
lines changed

2 files changed

+27
-4
lines changed

.changeset/spicy-camels-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Optimize href() to avoid backtracking regex on splat

packages/react-router/lib/href.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@ export function href<Path extends keyof Args>(
2727
...args: Args[Path]
2828
): string {
2929
let params = args[0];
30-
let result = path
31-
.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
30+
let result = trimTrailingSplat(path) // Ignore trailing / and /*, we'll handle it below
3231
.replace(
3332
/\/:([\w-]+)(\?)?/g, // same regex as in .\router\utils.ts: compilePath().
3433
(_: string, param: string, questionMark: string | undefined) => {
3534
const isRequired = questionMark === undefined;
36-
const value = params ? params[param] : undefined;
35+
const value = params?.[param];
3736
if (isRequired && value === undefined) {
3837
throw new Error(
3938
`Path '${path}' requires param '${param}' but it was not provided`,
@@ -46,11 +45,30 @@ export function href<Path extends keyof Args>(
4645
if (path.endsWith("*")) {
4746
// treat trailing splat the same way as compilePath, and force it to be as if it were `/*`.
4847
// `react-router typegen` will not generate the params for a malformed splat, causing a type error, but we can still do the correct thing here.
49-
const value = params ? params["*"] : undefined;
48+
const value = params?.["*"];
5049
if (value !== undefined) {
5150
result += "/" + value;
5251
}
5352
}
5453

5554
return result || "/";
5655
}
56+
57+
/**
58+
* Removes a trailing splat and any number of slashes from the end of the path.
59+
*
60+
* Benchmarked to be faster than `path.replace(/\/*\*?$/, "")`, which backtracks.
61+
*/
62+
function trimTrailingSplat(path: string): string {
63+
let i = path.length - 1;
64+
let char = path[i];
65+
if (char !== "*" && char !== "/") return path;
66+
67+
// for/break benchmarks faster than do/while
68+
i--;
69+
for (; i >= 0; i--) {
70+
if (path[i] !== "/") break;
71+
}
72+
73+
return path.slice(0, i + 1);
74+
}

0 commit comments

Comments
 (0)