Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 85d4eb9

Browse filesBrowse files
committed
perf: memoize blobs requests in the request scope
1 parent 2f7dee1 commit 85d4eb9
Copy full SHA for 85d4eb9

File tree

Expand file treeCollapse file tree

8 files changed

+431
-77
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+431
-77
lines changed

‎package-lock.json

Copy file name to clipboardExpand all lines: package-lock.json
+30-46Lines changed: 30 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Copy file name to clipboardExpand all lines: package.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"fs-monkey": "^1.0.6",
7777
"get-port": "^7.1.0",
7878
"lambda-local": "^2.2.0",
79+
"lru-cache": "^10.4.3",
7980
"memfs": "^4.9.2",
8081
"mock-require": "^3.0.3",
8182
"msw": "^2.0.7",

‎src/run/config.ts

Copy file name to clipboardExpand all lines: src/run/config.ts
+26-6Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path'
55
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
66

77
import { PLUGIN_DIR, RUN_CONFIG } from './constants.js'
8+
import { setInMemoryCacheMaxSizeFromNextConfig } from './regional-blob-store.cjs'
89

910
/**
1011
* Get Next.js config from the build output
@@ -13,10 +14,27 @@ export const getRunConfig = async () => {
1314
return JSON.parse(await readFile(resolve(PLUGIN_DIR, RUN_CONFIG), 'utf-8'))
1415
}
1516

17+
type NextConfigForMultipleVersions = NextConfigComplete & {
18+
experimental: NextConfigComplete['experimental'] & {
19+
// those are pre 14.1.0 options that were moved out of experimental in // https://github.com/vercel/next.js/pull/57953/files#diff-c49c4767e6ed8627e6e1b8f96b141ee13246153f5e9142e1da03450c8e81e96fL311
20+
21+
// https://github.com/vercel/next.js/blob/v14.0.4/packages/next/src/server/config-shared.ts#L182-L183
22+
// custom path to a cache handler to use
23+
incrementalCacheHandlerPath?: string
24+
// https://github.com/vercel/next.js/blob/v14.0.4/packages/next/src/server/config-shared.ts#L207-L212
25+
/**
26+
* In-memory cache size in bytes.
27+
*
28+
* If `isrMemoryCacheSize: 0` disables in-memory caching.
29+
*/
30+
isrMemoryCacheSize?: number
31+
}
32+
}
33+
1634
/**
1735
* Configure the custom cache handler at request time
1836
*/
19-
export const setRunConfig = (config: NextConfigComplete) => {
37+
export const setRunConfig = (config: NextConfigForMultipleVersions) => {
2038
const cacheHandler = join(PLUGIN_DIR, '.netlify/dist/run/handlers/cache.cjs')
2139
if (!existsSync(cacheHandler)) {
2240
throw new Error(`Cache handler not found at ${cacheHandler}`)
@@ -25,15 +43,17 @@ export const setRunConfig = (config: NextConfigComplete) => {
2543
// set the path to the cache handler
2644
config.experimental = {
2745
...config.experimental,
28-
// @ts-expect-error incrementalCacheHandlerPath was removed from config type
29-
// but we still need to set it for older Next.js versions
46+
// Before Next.js 14.1.0 path to the cache handler was in experimental section, see NextConfigForMultipleVersions type
3047
incrementalCacheHandlerPath: cacheHandler,
3148
}
3249

33-
// Next.js 14.1.0 moved the cache handler from experimental to stable
34-
// https://github.com/vercel/next.js/pull/57953/files#diff-c49c4767e6ed8627e6e1b8f96b141ee13246153f5e9142e1da03450c8e81e96fL311
50+
// Next.js 14.1.0 moved the cache handler from experimental to stable, see NextConfigForMultipleVersions type
3551
config.cacheHandler = cacheHandler
36-
config.cacheMaxMemorySize = 0
52+
53+
// honor the in-memory cache size from next.config (either one set by user or Next.js default)
54+
setInMemoryCacheMaxSizeFromNextConfig(
55+
config.cacheMaxMemorySize ?? config.experimental?.isrMemoryCacheSize,
56+
)
3757

3858
// set config
3959
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config)

‎src/run/handlers/cache.cts

Copy file name to clipboardExpand all lines: src/run/handlers/cache.cts
+9-17Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,18 @@ import {
3030
import { getLogger, getRequestContext } from './request-context.cjs'
3131
import { getTracer, recordWarning } from './tracer.cjs'
3232

33-
type TagManifestBlobCache = Record<string, Promise<TagManifest | null>>
34-
3533
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
3634

3735
export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
3836
options: CacheHandlerContext
3937
revalidatedTags: string[]
4038
cacheStore: MemoizedKeyValueStoreBackedByRegionalBlobStore
4139
tracer = getTracer()
42-
tagManifestsFetchedFromBlobStoreInCurrentRequest: TagManifestBlobCache
4340

4441
constructor(options: CacheHandlerContext) {
4542
this.options = options
4643
this.revalidatedTags = options.revalidatedTags
4744
this.cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
48-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest = {}
4945
}
5046

5147
private getTTL(blob: NetlifyCacheHandlerValue) {
@@ -469,7 +465,8 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
469465
}
470466

471467
resetRequestCache() {
472-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest = {}
468+
// no-op because in-memory cache is scoped to requests and not global
469+
// see getRequestSpecificInMemoryCache
473470
}
474471

475472
/**
@@ -508,10 +505,9 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
508505
}
509506

510507
// 2. If any in-memory tags don't indicate that any of tags was invalidated
511-
// we will check blob store, but memoize results for duration of current request
512-
// so that we only check blob store once per tag within a single request
513-
// full-route cache and fetch caches share a lot of tags so this might save
514-
// some roundtrips to the blob store.
508+
// we will check blob store. Full-route cache and fetch caches share a lot of tags
509+
// but we will only do actual blob read once withing a single request due to cacheStore
510+
// memoization.
515511
// Additionally, we will resolve the promise as soon as we find first
516512
// stale tag, so that we don't wait for all of them to resolve (but keep all
517513
// running in case future `CacheHandler.get` calls would be able to use results).
@@ -521,14 +517,10 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
521517
const tagManifestPromises: Promise<boolean>[] = []
522518

523519
for (const tag of cacheTags) {
524-
let tagManifestPromise: Promise<TagManifest | null> =
525-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest[tag]
526-
527-
if (!tagManifestPromise) {
528-
tagManifestPromise = this.cacheStore.get<TagManifest>(tag, 'tagManifest.get')
529-
530-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest[tag] = tagManifestPromise
531-
}
520+
const tagManifestPromise: Promise<TagManifest | null> = this.cacheStore.get<TagManifest>(
521+
tag,
522+
'tagManifest.get',
523+
)
532524

533525
tagManifestPromises.push(
534526
tagManifestPromise.then((tagManifest) => {

‎src/run/handlers/request-context.cts

Copy file name to clipboardExpand all lines: src/run/handlers/request-context.cts
+14-4Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,22 @@ export type RequestContext = {
3939
*/
4040
backgroundWorkPromise: Promise<unknown>
4141
logger: SystemLogger
42+
requestID: string
4243
}
4344

4445
type RequestContextAsyncLocalStorage = AsyncLocalStorage<RequestContext>
46+
const REQUEST_CONTEXT_GLOBAL_KEY = Symbol.for('nf-request-context-async-local-storage')
47+
const REQUEST_COUNTER_KEY = Symbol.for('nf-request-counter')
48+
const extendedGlobalThis = globalThis as typeof globalThis & {
49+
[REQUEST_CONTEXT_GLOBAL_KEY]?: RequestContextAsyncLocalStorage
50+
[REQUEST_COUNTER_KEY]?: number
51+
}
52+
53+
function getFallbackRequestID() {
54+
const requestNumber = extendedGlobalThis[REQUEST_COUNTER_KEY] ?? 0
55+
extendedGlobalThis[REQUEST_COUNTER_KEY] = requestNumber + 1
56+
return `#${requestNumber}`
57+
}
4558

4659
export function createRequestContext(request?: Request, context?: FutureContext): RequestContext {
4760
const backgroundWorkPromises: Promise<unknown>[] = []
@@ -72,10 +85,10 @@ export function createRequestContext(request?: Request, context?: FutureContext)
7285
return Promise.allSettled(backgroundWorkPromises)
7386
},
7487
logger,
88+
requestID: request?.headers.get('x-nf-request-id') ?? getFallbackRequestID(),
7589
}
7690
}
7791

78-
const REQUEST_CONTEXT_GLOBAL_KEY = Symbol.for('nf-request-context-async-local-storage')
7992
let requestContextAsyncLocalStorage: RequestContextAsyncLocalStorage | undefined
8093
function getRequestContextAsyncLocalStorage() {
8194
if (requestContextAsyncLocalStorage) {
@@ -85,9 +98,6 @@ function getRequestContextAsyncLocalStorage() {
8598
// AsyncLocalStorage in the module scope, because it will be different for each
8699
// copy - so first time an instance of this module is used, we store AsyncLocalStorage
87100
// in global scope and reuse it for all subsequent calls
88-
const extendedGlobalThis = globalThis as typeof globalThis & {
89-
[REQUEST_CONTEXT_GLOBAL_KEY]?: RequestContextAsyncLocalStorage
90-
}
91101
if (extendedGlobalThis[REQUEST_CONTEXT_GLOBAL_KEY]) {
92102
return extendedGlobalThis[REQUEST_CONTEXT_GLOBAL_KEY]
93103
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.