Skip to content

Commit

Permalink
Improve performance of String.prototype.split uses (#56746)
Browse files Browse the repository at this point in the history
This PR adds the optional `limit` parameter on String.prototype.split uses.

> If provided, splits the string at each occurrence of the specified separator, but stops when limit entries have been placed in the array. Any leftover text is not included in the array at all.

[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split#syntax)

While the performance gain may not be significant for small texts, it can be huge for large ones.

I made a benchmark on the following repository : https://github.com/Yovach/benchmark-nodejs

On my machine, I get the following results:
`node index.js`
> normal 1: 570.092ms
> normal 50: 2.284s
> normal 100: 3.543s

`node index-optimized.js`
> optmized 1: 644.301ms
> optmized 50: 929.39ms
> optmized 100: 1.020s

The "benchmarks" numbers are : 
- "lorem-1" file contains 1 paragraph of "lorem ipsum"
- "lorem-50" file contains 50 paragraphes of "lorem ipsum"
- "lorem-100" file contains 100 paragraphes of "lorem ipsum"
  • Loading branch information
Yovach authored Oct 19, 2023
1 parent 8e20345 commit abe8b1e
Show file tree
Hide file tree
Showing 45 changed files with 63 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .github/actions/validate-docs-links/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async function prepareDocumentMapEntry(
// Checks if the links point to existing documents
function validateInternalLink(errors: Errors, href: string): void {
// /docs/api/example#heading -> ["api/example", "heading""]
const [link, hash] = href.replace(DOCS_PATH, '').split('#')
const [link, hash] = href.replace(DOCS_PATH, '').split('#', 2)

let foundPage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export = defineRule({
hrefValue.startsWith('https://fonts.googleapis.com/css')

if (isGoogleFont) {
const params = new URLSearchParams(hrefValue.split('?')[1])
const params = new URLSearchParams(hrefValue.split('?', 2)[1])
const displayValue = params.get('display')

if (!params.has('display')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export = defineRule({
return
}

const document = context.getFilename().split('pages')[1]
const document = context.getFilename().split('pages', 2)[1]
if (document && path.parse(document).name.startsWith('_document')) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export = defineRule({
return
}

const document = context.getFilename().split('pages')[1]
const document = context.getFilename().split('pages', 2)[1]
if (!document) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export = defineRule({
create(context) {
return {
JSXOpeningElement(node) {
const document = context.getFilename().split('pages')[1]
const document = context.getFilename().split('pages', 2)[1]
if (!document) {
return
}
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin-next/src/rules/no-typos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export = defineRule({
}
return {
ExportNamedDeclaration(node) {
const page = context.getFilename().split('pages')[1]
const page = context.getFilename().split('pages', 2)[1]
if (!page || path.parse(page).dir.startsWith('/api')) {
return
}
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint-plugin-next/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export function normalizeURL(url: string) {
if (!url) {
return
}
url = url.split('?')[0]
url = url.split('#')[0]
url = url.split('?', 1)[0]
url = url.split('#', 1)[0]
url = url = url.replace(/(\/index\.html)$/, '/')
// Empty URLs should not be trailed with `/`, e.g. `#heading`
if (url === '') {
Expand Down
2 changes: 1 addition & 1 deletion packages/font/src/google/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const nextFontGoogleFontLoader: FontLoader = async ({
}

// CSS Variables may be set on a body tag, ignore them to keep the CSS module pure
fontFaceDeclarations = fontFaceDeclarations.split('body {')[0]
fontFaceDeclarations = fontFaceDeclarations.split('body {', 1)[0]

// Find font files to download, provide the array of subsets we want to preload if preloading is enabled
const fontFiles = findFontFilesInCss(
Expand Down
4 changes: 2 additions & 2 deletions packages/font/src/google/sort-fonts-variant-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export function sortFontsVariantValues(valA: string, valB: string) {
// If both values contain commas, it indicates they are in "ital,wght" format
if (valA.includes(',') && valB.includes(',')) {
// Split the values into prefix and suffix
const [aPrefix, aSuffix] = valA.split(',')
const [bPrefix, bSuffix] = valB.split(',')
const [aPrefix, aSuffix] = valA.split(',', 2)
const [bPrefix, bSuffix] = valB.split(',', 2)

// Compare the prefixes (ital values)
if (aPrefix === bPrefix) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,7 @@ const plugin = (options: any = {}) => {
return parsedDeclaration
}

const splittedUrl = url.split(/(\?)?#/)
const [pathname, query, hashOrQuery] = splittedUrl
const [pathname, query, hashOrQuery] = url.split(/(\?)?#/, 3)

let hash = query ? '?' : ''
hash += hashOrQuery ? `#${hashOrQuery}` : ''
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async function createTreeCodeFromPath(
rootLayout: string | undefined
globalError: string | undefined
}> {
const splittedPath = pagePath.split(/[\\/]/)
const splittedPath = pagePath.split(/[\\/]/, 1)
const isNotFoundRoute = page === '/_not-found'
const isDefaultNotFound = isAppBuiltinNotFoundPage(pagePath)
const appDirPrefix = isDefaultNotFound ? APP_DIR_ALIAS : splittedPath[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type MetadataRouteLoaderOptions = {

export function getFilenameAndExtension(resourcePath: string) {
const filename = path.basename(resourcePath)
const [name, ext] = filename.split('.')
const [name, ext] = filename.split('.', 2)
return { name, ext }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default async function resolveUrlLoader(
(typeof exception === 'string' && exception) ||
(exception instanceof Error && [
exception.message,
(exception as any).stack.split('\n')[1].trim(),
(exception as any).stack.split('\n', 2)[1].trim(),
]) ||
[]
)
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/client/app-link-gc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function linkGc() {
if (link.dataset.precedence?.startsWith('next')) {
const href = link.getAttribute('href')
if (href) {
const [resource, version] = href.split('?v=')
const [resource, version] = href.split('?v=', 2)
if (version) {
const currentOrigin = window.location.origin
const allLinks = [
Expand All @@ -35,7 +35,7 @@ export function linkGc() {
if (otherLink.dataset.precedence?.startsWith('next')) {
const otherHref = otherLink.getAttribute('href')
if (otherHref) {
const [, otherVersion] = otherHref.split('?v=')
const [, otherVersion] = otherHref.split('?v=', 2)
if (!otherVersion || +otherVersion < +version) {
// Delay the removal of the stylesheet to avoid FOUC
// caused by `@font-face` rules, as they seem to be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type TerminalProps = { content: string }
function getFile(lines: string[]) {
const contentFileName = lines.shift()
if (!contentFileName) return null
const [fileName, line, column] = contentFileName.split(':')
const [fileName, line, column] = contentFileName.split(':', 3)

const parsedLine = Number(line)
const parsedColumn = Number(column)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function getSocketUrl(assetPrefix: string): string {
}`

if (normalizedAssetPrefix.startsWith('http')) {
url = `${protocol}://${normalizedAssetPrefix.split('://')[1]}`
url = `${protocol}://${normalizedAssetPrefix.split('://', 2)[1]}`
}

return url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function parseComponentStack(
/^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/,
''
)
const [file, lineNumber, column] = modulePath?.split(':') ?? []
const [file, lineNumber, column] = modulePath?.split(':', 3) ?? []

componentStackFrames.push({
component,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/components/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,5 @@ export function getRedirectTypeFromError<U extends string>(
throw new Error('Not a redirect error')
}

return error.digest.split(';', 3)[1] as RedirectType
return error.digest.split(';', 2)[1] as RedirectType
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export function handleMutable(
false,
onlyHashChange:
!!mutable.hashFragment &&
state.canonicalUrl.split('#')[0] ===
mutable.canonicalUrl?.split('#')[0],
state.canonicalUrl.split('#', 1)[0] ===
mutable.canonicalUrl?.split('#', 1)[0],
hashFragment: shouldScroll
? // Empty hash should trigger default behavior of scrolling layout into view.
// #top is handled in layout-router.
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/dev/dev-build-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function initializeBuildWatcher(
position = 'bottom-right'
) {
const shadowHost = document.createElement('div')
const [verticalProperty, horizontalProperty] = position.split('-') as [
const [verticalProperty, horizontalProperty] = position.split('-', 2) as [
VerticalPosition,
HorizonalPosition
]
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/dev/error-overlay/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function connectHMR(options: { path: string; assetPrefix: string }) {
}`

if (assetPrefix.startsWith('http')) {
url = `${protocol}://${assetPrefix.split('://')[1]}`
url = `${protocol}://${assetPrefix.split('://', 2)[1]}`
}

source = new window.WebSocket(`${url}${options.path}`)
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/image-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function handleLoading(
function getDynamicProps(
fetchPriority?: string
): Record<string, string | undefined> {
const [majorStr, minorStr] = version.split('.')
const [majorStr, minorStr] = version.split('.', 2)
const major = parseInt(majorStr, 10)
const minor = parseInt(minorStr, 10)
if (major > 18 || (major === 18 && minor >= 3)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/resolve-href.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function resolveHref(
? urlAsString.slice(urlProtoMatch[0].length)
: urlAsString

const urlParts = urlAsStringNoProto.split('?')
const urlParts = urlAsStringNoProto.split('?', 1)

if ((urlParts[0] || '').match(/(\/\/|\\)/)) {
console.error(
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function getForwardedHeaders(
Array.isArray(rawSetCookies) ? rawSetCookies : [rawSetCookies]
).map((setCookie) => {
// remove the suffixes like 'HttpOnly' and 'SameSite'
const [cookie] = `${setCookie}`.split(';')
const [cookie] = `${setCookie}`.split(';', 1)
return cookie
})

Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {

if (this.i18nProvider) {
// Remove the port from the hostname if present.
const hostname = req?.headers.host?.split(':')[0].toLowerCase()
const hostname = req?.headers.host?.split(':', 1)[0].toLowerCase()

const domainLocale = this.i18nProvider.detectDomainLocale(hostname)
const defaultLocale =
Expand Down Expand Up @@ -792,7 +792,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
return origSetHeader(name, val)
}

const urlParts = (req.url || '').split('?')
const urlParts = (req.url || '').split('?', 1)
const urlNoQuery = urlParts[0]

// this normalizes repeated slashes in the path e.g. hello//world ->
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/future/helpers/i18n-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class I18NProvider {
const domain = domainLocale.domain.toLowerCase()
return {
defaultLocale: domainLocale.defaultLocale.toLowerCase(),
hostname: domain.split(':')[0],
hostname: domain.split(':', 1)[0],
domain,
locales: domainLocale.locales?.map((locale) => locale.toLowerCase()),
http: domainLocale.http,
Expand Down Expand Up @@ -154,7 +154,7 @@ export class I18NProvider {

// The first segment will be empty, because it has a leading `/`. If
// there is no further segment, there is no locale (or it's the default).
const segments = pathname.split('/')
const segments = pathname.split('/', 2)
if (!segments[1])
return {
detectedLocale,
Expand Down
8 changes: 4 additions & 4 deletions packages/next/src/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export class ImageOptimizerCache {
const now = Date.now()

for (const file of files) {
const [maxAgeSt, expireAtSt, etag, extension] = file.split('.')
const [maxAgeSt, expireAtSt, etag, extension] = file.split('.', 4)
const buffer = await promises.readFile(join(cacheDir, file))
const expireAt = Number(expireAtSt)
const maxAge = Number(maxAgeSt)
Expand Down Expand Up @@ -373,7 +373,7 @@ function parseCacheControl(str: string | null): Map<string, string> {
return map
}
for (let directive of str.split(',')) {
let [key, value] = directive.trim().split('=')
let [key, value] = directive.trim().split('=', 2)
key = key.toLowerCase()
if (value) {
value = value.toLowerCase()
Expand Down Expand Up @@ -686,13 +686,13 @@ function getFileNameWithExtension(
url: string,
contentType: string | null
): string {
const [urlWithoutQueryParams] = url.split('?')
const [urlWithoutQueryParams] = url.split('?', 1)
const fileNameWithExtension = urlWithoutQueryParams.split('/').pop()
if (!contentType || !fileNameWithExtension) {
return 'image.bin'
}

const [fileName] = fileNameWithExtension.split('.')
const [fileName] = fileNameWithExtension.split('.', 1)
const extension = getExtension(contentType)
return `${fileName}.${extension}`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function getResolveRoutes(
let parsedUrl = url.parse(req.url || '', true) as NextUrlWithParsedQuery
let didRewrite = false

const urlParts = (req.url || '').split('?')
const urlParts = (req.url || '').split('?', 1)
const urlNoQuery = urlParts[0]

// this normalizes repeated slashes in the path e.g. hello//world ->
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const getDebugPort = () => {
localArg.startsWith('--inspect') ||
localArg.startsWith('--inspect-brk')
)
?.split('=')[1] ??
?.split('=', 2)[1] ??
process.env.NODE_OPTIONS?.match?.(/--inspect(-brk)?(=(\S+))?( |$)/)?.[3]
return debugPortStr ? parseInt(debugPortStr, 10) : 9229
}
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,8 @@ export async function renderToHTMLImpl(
}

const [renderTargetPrefix, renderTargetSuffix] = documentHTML.split(
'<next-js-internal-body-render-target></next-js-internal-body-render-target>'
'<next-js-internal-body-render-target></next-js-internal-body-render-target>',
2
)

let prefix = ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ export async function continueFizzStream(
const closeTag = '</body></html>'

// Suffix itself might contain close tags at the end, so we need to split it.
const suffixUnclosed = suffix ? suffix.split(closeTag)[0] : null
const suffixUnclosed = suffix ? suffix.split(closeTag, 1)[0] : null

if (generateStaticHTML) {
await renderStream.allReady
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/shared/lib/get-hostname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function getHostname(
// hostname.
let hostname: string
if (headers?.host && !Array.isArray(headers.host)) {
hostname = headers.host.toString().split(':')[0]
hostname = headers.host.toString().split(':', 1)[0]
} else if (parsed.hostname) {
hostname = parsed.hostname
} else return
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/shared/lib/i18n/detect-domain-locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function detectDomainLocale(

for (const item of domainItems) {
// remove port if present
const domainHostname = item.domain?.split(':')[0].toLowerCase()
const domainHostname = item.domain?.split(':', 1)[0].toLowerCase()
if (
hostname === domainHostname ||
detectedLocale === item.defaultLocale.toLowerCase() ||
Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/shared/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2219,8 +2219,8 @@ export default class Router implements BaseRouter {

onlyAHashChange(as: string): boolean {
if (!this.asPath) return false
const [oldUrlNoHash, oldHash] = this.asPath.split('#')
const [newUrlNoHash, newHash] = as.split('#')
const [oldUrlNoHash, oldHash] = this.asPath.split('#', 2)
const [newUrlNoHash, newHash] = as.split('#', 2)

// Makes sure we scroll to the provided hash if the url/hash are the same
if (newHash && oldUrlNoHash === newUrlNoHash && oldHash === newHash) {
Expand All @@ -2240,7 +2240,7 @@ export default class Router implements BaseRouter {
}

scrollToHash(as: string): void {
const [, hash = ''] = as.split('#')
const [, hash = ''] = as.split('#', 2)

handleSmoothScroll(
() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function matchHas(
case 'host': {
const { host } = req?.headers || {}
// remove port from host if present
const hostname = host?.split(':')[0].toLowerCase()
const hostname = host?.split(':', 1)[0].toLowerCase()
value = hostname
break
}
Expand Down Expand Up @@ -257,7 +257,7 @@ export function prepareDestination(args: {
try {
newUrl = destPathCompiler(args.params)

const [pathname, hash] = newUrl.split('#')
const [pathname, hash] = newUrl.split('#', 2)
parsedDestination.hostname = destHostnameCompiler(args.params)
parsedDestination.pathname = pathname
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
Expand Down
Loading

0 comments on commit abe8b1e

Please sign in to comment.