Skip to content

Commit aafb0a5

Browse files
committed
optimize performance
1 parent b0321c5 commit aafb0a5

File tree

1 file changed

+103
-32
lines changed

1 file changed

+103
-32
lines changed

packages/nextjs/src/client/routing/parameterization.ts

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const globalWithInjectedManifest = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
66
_sentryRouteManifest: RouteManifest | undefined;
77
};
88

9+
// Some performance caches
10+
let cachedManifest: RouteManifest | null = null;
11+
let cachedManifestString: string | undefined = undefined;
12+
const compiledRegexCache: Map<string, RegExp> = new Map();
13+
const routeResultCache: Map<string, string | undefined> = new Map();
14+
915
/**
1016
* Calculate the specificity score for a route path.
1117
* Lower scores indicate more specific routes.
@@ -35,65 +41,130 @@ function getRouteSpecificity(routePath: string): number {
3541
}
3642

3743
/**
38-
* Parameterize a route using the route manifest.
39-
*
40-
* @param route - The route to parameterize.
41-
* @returns The parameterized route or undefined if no parameterization is needed.
44+
* Get compiled regex from cache or create and cache it.
4245
*/
43-
export const maybeParameterizeRoute = (route: string): string | undefined => {
46+
function getCompiledRegex(regexString: string): RegExp | null {
47+
if (compiledRegexCache.has(regexString)) {
48+
return compiledRegexCache.get(regexString) ?? null;
49+
}
50+
51+
try {
52+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regex patterns are from build-time route manifest, not user input
53+
const regex = new RegExp(regexString);
54+
compiledRegexCache.set(regexString, regex);
55+
return regex;
56+
} catch (error) {
57+
DEBUG_BUILD && logger.warn('Could not compile regex', { regexString, error });
58+
// Cache the failure to avoid repeated attempts by storing undefined
59+
return null;
60+
}
61+
}
62+
63+
/**
64+
* Get and cache the route manifest from the global object.
65+
* @returns The parsed route manifest or null if not available/invalid.
66+
*/
67+
function getManifest(): RouteManifest | null {
4468
if (
45-
!globalWithInjectedManifest._sentryRouteManifest ||
69+
!globalWithInjectedManifest?._sentryRouteManifest ||
4670
typeof globalWithInjectedManifest._sentryRouteManifest !== 'string'
4771
) {
48-
return undefined;
72+
return null;
73+
}
74+
75+
const currentManifestString = globalWithInjectedManifest._sentryRouteManifest;
76+
77+
// Return cached manifest if the string hasn't changed
78+
if (cachedManifest && cachedManifestString === currentManifestString) {
79+
return cachedManifest;
4980
}
5081

82+
// Clear caches when manifest changes
83+
compiledRegexCache.clear();
84+
routeResultCache.clear();
85+
5186
let manifest: RouteManifest = {
5287
staticRoutes: [],
5388
dynamicRoutes: [],
5489
};
5590

5691
// Shallow check if the manifest is actually what we expect it to be
5792
try {
58-
manifest = JSON.parse(globalWithInjectedManifest._sentryRouteManifest);
93+
manifest = JSON.parse(currentManifestString);
5994
if (!Array.isArray(manifest.staticRoutes) || !Array.isArray(manifest.dynamicRoutes)) {
60-
return undefined;
95+
return null;
6196
}
97+
// Cache the successfully parsed manifest
98+
cachedManifest = manifest;
99+
cachedManifestString = currentManifestString;
100+
return manifest;
62101
} catch (error) {
63-
DEBUG_BUILD && logger.warn('Could not extract route manifest');
64102
// Something went wrong while parsing the manifest, so we'll fallback to no parameterization
65-
return undefined;
66-
}
67-
68-
// Static path: no parameterization needed
69-
if (manifest.staticRoutes.some(r => r.path === route)) {
70-
return undefined;
103+
DEBUG_BUILD && logger.warn('Could not extract route manifest');
104+
return null;
71105
}
106+
}
72107

108+
/**
109+
* Find matching routes from static and dynamic route collections.
110+
* @param route - The route to match against.
111+
* @param staticRoutes - Array of static route objects.
112+
* @param dynamicRoutes - Array of dynamic route objects.
113+
* @returns Array of matching route paths.
114+
*/
115+
function findMatchingRoutes(
116+
route: string,
117+
staticRoutes: RouteManifest['staticRoutes'],
118+
dynamicRoutes: RouteManifest['dynamicRoutes'],
119+
): string[] {
73120
const matches: string[] = [];
74121

122+
// Static path: no parameterization needed, return empty array
123+
if (staticRoutes.some(r => r.path === route)) {
124+
return matches;
125+
}
126+
75127
// Dynamic path: find the route pattern that matches the concrete route
76-
for (const dynamicRoute of manifest.dynamicRoutes) {
128+
for (const dynamicRoute of dynamicRoutes) {
77129
if (dynamicRoute.regex) {
78-
try {
79-
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- regex patterns are from build-time route manifest, not user input
80-
const regex = new RegExp(dynamicRoute.regex);
81-
if (regex.test(route)) {
82-
matches.push(dynamicRoute.path);
83-
}
84-
} catch (error) {
85-
// Just skip this route in case of invalid regex
86-
continue;
130+
const regex = getCompiledRegex(dynamicRoute.regex);
131+
if (regex?.test(route)) {
132+
matches.push(dynamicRoute.path);
87133
}
88134
}
89135
}
90136

91-
if (matches.length === 1) {
92-
return matches[0];
93-
} else if (matches.length > 1) {
94-
// Only calculate specificity when we have multiple matches like [param] and [...params]
95-
return matches.sort((a, b) => getRouteSpecificity(a) - getRouteSpecificity(b))[0];
137+
return matches;
138+
}
139+
140+
/**
141+
* Parameterize a route using the route manifest.
142+
*
143+
* @param route - The route to parameterize.
144+
* @returns The parameterized route or undefined if no parameterization is needed.
145+
*/
146+
export const maybeParameterizeRoute = (route: string): string | undefined => {
147+
const manifest = getManifest();
148+
if (!manifest) {
149+
return undefined;
150+
}
151+
152+
// Check route result cache after manifest validation
153+
if (routeResultCache.has(route)) {
154+
return routeResultCache.get(route);
96155
}
97156

98-
return undefined;
157+
const { staticRoutes, dynamicRoutes } = manifest;
158+
if (!Array.isArray(staticRoutes) || !Array.isArray(dynamicRoutes)) {
159+
return undefined;
160+
}
161+
162+
const matches = findMatchingRoutes(route, staticRoutes, dynamicRoutes);
163+
164+
// We can always do the `sort()` call, it will short-circuit when it has one array item
165+
const result = matches.sort((a, b) => getRouteSpecificity(a) - getRouteSpecificity(b))[0];
166+
167+
routeResultCache.set(route, result);
168+
169+
return result;
99170
};

0 commit comments

Comments
 (0)