Skip to content

Commit 0014161

Browse files
authored
Optimize isMetadataRoute function (#84409)
## What? Had Claude-4-Sonnet do a pass on optimizing this function. Let's see if the tests pass.
1 parent a07a33a commit 0014161

File tree

1 file changed

+111
-40
lines changed

1 file changed

+111
-40
lines changed

packages/next/src/lib/metadata/is-metadata-route.ts

Lines changed: 111 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,19 @@ export const DEFAULT_METADATA_ROUTE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx']
3232

3333
// Match the file extension with the dynamic multi-routes extensions
3434
// e.g. ([xml, js], null) -> can match `/sitemap.xml/route`, `sitemap.js/route`
35-
// e.g. ([png], [ts]) -> can match `/opengrapg-image.png`, `/opengraph-image.ts`
35+
// e.g. ([png], [ts]) -> can match `/opengraph-image.png`, `/opengraph-image.ts`
3636
export const getExtensionRegexString = (
3737
staticExtensions: readonly string[],
3838
dynamicExtensions: readonly string[] | null
3939
) => {
40+
let result: string
4041
// If there's no possible multi dynamic routes, will not match any <name>[].<ext> files
4142
if (!dynamicExtensions || dynamicExtensions.length === 0) {
42-
return `(\\.(?:${staticExtensions.join('|')}))`
43+
result = `(\\.(?:${staticExtensions.join('|')}))`
44+
} else {
45+
result = `(?:\\.(${staticExtensions.join('|')})|(\\.(${dynamicExtensions.join('|')})))`
4346
}
44-
return `(?:\\.(${staticExtensions.join('|')})|(\\.(${dynamicExtensions.join('|')})))`
47+
return result
4548
}
4649

4750
/**
@@ -53,79 +56,147 @@ export function isStaticMetadataFile(appDirRelativePath: string) {
5356
return isMetadataRouteFile(appDirRelativePath, [], true)
5457
}
5558

56-
/**
57-
* Determine if the file is a metadata route file entry
58-
* @param appDirRelativePath the relative file path to app/
59-
* @param pageExtensions the js extensions, such as ['js', 'jsx', 'ts', 'tsx']
60-
* @param strictlyMatchExtensions if it's true, match the file with page extension, otherwise match the file with default corresponding extension
61-
* @returns if the file is a metadata route file
62-
*/
63-
export function isMetadataRouteFile(
64-
appDirRelativePath: string,
59+
// Pre-compiled static regexes for common cases
60+
const FAVICON_REGEX = /^[\\/]favicon\.ico$/
61+
const ROBOTS_TXT_REGEX = /^[\\/]robots\.txt$/
62+
const MANIFEST_JSON_REGEX = /^[\\/]manifest\.json$/
63+
const MANIFEST_WEBMANIFEST_REGEX = /^[\\/]manifest\.webmanifest$/
64+
const SITEMAP_XML_REGEX = /[\\/]sitemap\.xml$/
65+
66+
// Cache for compiled regex patterns based on parameters
67+
const compiledRegexCache = new Map<string, RegExp[]>()
68+
69+
// Fast path checks for common metadata files
70+
function fastPathCheck(normalizedPath: string): boolean | null {
71+
// Check favicon.ico first (most common)
72+
if (FAVICON_REGEX.test(normalizedPath)) return true
73+
74+
// Check other common static files
75+
if (ROBOTS_TXT_REGEX.test(normalizedPath)) return true
76+
if (MANIFEST_JSON_REGEX.test(normalizedPath)) return true
77+
if (MANIFEST_WEBMANIFEST_REGEX.test(normalizedPath)) return true
78+
if (SITEMAP_XML_REGEX.test(normalizedPath)) return true
79+
80+
// Quick negative check - if it doesn't contain any metadata keywords, skip
81+
if (
82+
!normalizedPath.includes('robots') &&
83+
!normalizedPath.includes('manifest') &&
84+
!normalizedPath.includes('sitemap') &&
85+
!normalizedPath.includes('icon') &&
86+
!normalizedPath.includes('apple-icon') &&
87+
!normalizedPath.includes('opengraph-image') &&
88+
!normalizedPath.includes('twitter-image') &&
89+
!normalizedPath.includes('favicon')
90+
) {
91+
return false
92+
}
93+
94+
return null // Continue with full regex matching
95+
}
96+
97+
function getCompiledRegexes(
6598
pageExtensions: PageExtensions,
6699
strictlyMatchExtensions: boolean
67-
) {
68-
// End with the extension or optional to have the extension
69-
// When strictlyMatchExtensions is true, it's used for match file path;
70-
// When strictlyMatchExtensions, the dynamic extension is skipped but
71-
// static extension is kept, which is usually used for matching route path.
72-
const trailingMatcher = (strictlyMatchExtensions ? '' : '?') + '$'
73-
// Match the optional variants like /opengraph-image2, /icon-a102f4.png, etc.
100+
): RegExp[] {
101+
// Create cache key
102+
const cacheKey = `${pageExtensions.join(',')}|${strictlyMatchExtensions}`
103+
104+
const cached = compiledRegexCache.get(cacheKey)
105+
if (cached) {
106+
return cached
107+
}
108+
109+
// Pre-compute common strings
110+
const trailingMatcher = strictlyMatchExtensions ? '$' : '?$'
74111
const variantsMatcher = '\\d?'
75-
// The -\w{6} is the suffix that normalized from group routes;
76112
const groupSuffix = strictlyMatchExtensions ? '' : '(-\\w{6})?'
113+
const suffixMatcher = variantsMatcher + groupSuffix
77114

78-
const suffixMatcher = `${variantsMatcher}${groupSuffix}`
115+
// Pre-compute extension arrays to avoid repeated concatenation
116+
const robotsExts =
117+
pageExtensions.length > 0 ? [...pageExtensions, 'txt'] : ['txt']
118+
const manifestExts =
119+
pageExtensions.length > 0
120+
? [...pageExtensions, 'webmanifest', 'json']
121+
: ['webmanifest', 'json']
79122

80-
const metadataRouteFilesRegex = [
123+
const regexes = [
81124
new RegExp(
82-
`^[\\\\/]robots${getExtensionRegexString(
83-
pageExtensions.concat('txt'),
84-
null
85-
)}${trailingMatcher}`
125+
`^[\\\\/]robots${getExtensionRegexString(robotsExts, null)}${trailingMatcher}`
86126
),
87127
new RegExp(
88-
`^[\\\\/]manifest${getExtensionRegexString(
89-
pageExtensions.concat('webmanifest', 'json'),
90-
null
91-
)}${trailingMatcher}`
128+
`^[\\\\/]manifest${getExtensionRegexString(manifestExts, null)}${trailingMatcher}`
92129
),
93-
new RegExp(`^[\\\\/]favicon\\.ico$`),
130+
// FAVICON_REGEX removed - already handled in fastPathCheck
94131
new RegExp(
95132
`[\\\\/]sitemap${getExtensionRegexString(['xml'], pageExtensions)}${trailingMatcher}`
96133
),
97134
new RegExp(
98-
`[\\\\/]${STATIC_METADATA_IMAGES.icon.filename}${suffixMatcher}${getExtensionRegexString(
135+
`[\\\\/]icon${suffixMatcher}${getExtensionRegexString(
99136
STATIC_METADATA_IMAGES.icon.extensions,
100137
pageExtensions
101138
)}${trailingMatcher}`
102139
),
103140
new RegExp(
104-
`[\\\\/]${STATIC_METADATA_IMAGES.apple.filename}${suffixMatcher}${getExtensionRegexString(
141+
`[\\\\/]apple-icon${suffixMatcher}${getExtensionRegexString(
105142
STATIC_METADATA_IMAGES.apple.extensions,
106143
pageExtensions
107144
)}${trailingMatcher}`
108145
),
109146
new RegExp(
110-
`[\\\\/]${STATIC_METADATA_IMAGES.openGraph.filename}${suffixMatcher}${getExtensionRegexString(
147+
`[\\\\/]opengraph-image${suffixMatcher}${getExtensionRegexString(
111148
STATIC_METADATA_IMAGES.openGraph.extensions,
112149
pageExtensions
113150
)}${trailingMatcher}`
114151
),
115152
new RegExp(
116-
`[\\\\/]${STATIC_METADATA_IMAGES.twitter.filename}${suffixMatcher}${getExtensionRegexString(
153+
`[\\\\/]twitter-image${suffixMatcher}${getExtensionRegexString(
117154
STATIC_METADATA_IMAGES.twitter.extensions,
118155
pageExtensions
119156
)}${trailingMatcher}`
120157
),
121158
]
122159

123-
const normalizedAppDirRelativePath = normalizePathSep(appDirRelativePath)
124-
const matched = metadataRouteFilesRegex.some((r) =>
125-
r.test(normalizedAppDirRelativePath)
126-
)
160+
compiledRegexCache.set(cacheKey, regexes)
161+
return regexes
162+
}
127163

128-
return matched
164+
/**
165+
* Determine if the file is a metadata route file entry
166+
* @param appDirRelativePath the relative file path to app/
167+
* @param pageExtensions the js extensions, such as ['js', 'jsx', 'ts', 'tsx']
168+
* @param strictlyMatchExtensions if it's true, match the file with page extension, otherwise match the file with default corresponding extension
169+
* @returns if the file is a metadata route file
170+
*/
171+
export function isMetadataRouteFile(
172+
appDirRelativePath: string,
173+
pageExtensions: PageExtensions,
174+
strictlyMatchExtensions: boolean
175+
): boolean {
176+
// Early exit for empty or obviously non-metadata paths
177+
if (!appDirRelativePath || appDirRelativePath.length < 2) {
178+
return false
179+
}
180+
181+
const normalizedPath = normalizePathSep(appDirRelativePath)
182+
183+
// Fast path check for common cases
184+
const fastResult = fastPathCheck(normalizedPath)
185+
if (fastResult !== null) {
186+
return fastResult
187+
}
188+
189+
// Get compiled regexes from cache
190+
const regexes = getCompiledRegexes(pageExtensions, strictlyMatchExtensions)
191+
192+
// Use for loop instead of .some() for better performance
193+
for (let i = 0; i < regexes.length; i++) {
194+
if (regexes[i].test(normalizedPath)) {
195+
return true
196+
}
197+
}
198+
199+
return false
129200
}
130201

131202
// Check if the route is a static metadata route, with /route suffix

0 commit comments

Comments
 (0)