Skip to content

Commit 2d55e8f

Browse files
authored
Cache loading and update optimization (#756)
1 parent fa02080 commit 2d55e8f

File tree

3 files changed

+66
-13
lines changed

3 files changed

+66
-13
lines changed

.changeset/fast-points-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudflare/next-on-pages': minor
3+
---
4+
5+
Optimized cache tags manifest loading; cache updates do not block responses

packages/next-on-pages/templates/_worker.js/utils/cache.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const CACHE_TAGS_HEADER = 'x-vercel-cache-tags';
66
// https://github.com/vercel/next.js/blob/ba23d986/packages/next/src/lib/constants.ts#L18
77
const NEXT_CACHE_SOFT_TAGS_HEADER = 'x-next-cache-soft-tags';
88

9+
const REQUEST_CONTEXT_KEY = Symbol.for('__cloudflare-request-context__');
10+
911
/**
1012
* Handles an internal request to the suspense cache.
1113
*
@@ -58,15 +60,29 @@ export async function handleSuspenseCacheRequest(request: Request) {
5860
});
5961
}
6062
case 'POST': {
61-
// Update the value in the cache.
62-
const body = await request.json<IncrementalCacheValue>();
63-
// Falling back to the cache tags header for Next.js 13.5+
64-
if (body.data.tags === undefined) {
65-
body.tags ??= getTagsFromHeader(request, CACHE_TAGS_HEADER) ?? [];
63+
// Retrieve request context.
64+
const reqCtx = (globalThis as unknown as Record<symbol, unknown>)[
65+
REQUEST_CONTEXT_KEY
66+
] as { ctx: ExecutionContext };
67+
68+
const update = async () => {
69+
// Update the value in the cache.
70+
const body = await request.json<IncrementalCacheValue>();
71+
// Falling back to the cache tags header for Next.js 13.5+
72+
if (body.data.tags === undefined) {
73+
body.tags ??= getTagsFromHeader(request, CACHE_TAGS_HEADER) ?? [];
74+
}
75+
76+
await cache.set(cacheKey, body);
77+
};
78+
79+
if (reqCtx) {
80+
// Avoid waiting for the cache to update before responding, if possible.
81+
reqCtx.ctx.waitUntil(update());
82+
} else {
83+
await update();
6684
}
6785

68-
await cache.set(cacheKey, body);
69-
7086
return new Response(null, { status: 200 });
7187
}
7288
default:

packages/next-on-pages/templates/cache/adaptor.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export class CacheAdaptor {
1313
public tagsManifest: TagsManifest | undefined;
1414
/** The key used for the tags manifest in the cache. */
1515
public tagsManifestKey = 'tags-manifest';
16+
/** Promise that resolves when tags manifest is loaded */
17+
public tagsManifestPromise: Promise<void> | undefined;
1618

1719
/**
1820
* @param ctx The incremental cache context from Next.js. NOTE: This is not currently utilised in NOP.
@@ -52,7 +54,7 @@ export class CacheAdaptor {
5254
};
5355

5456
// Update the cache entry.
55-
await this.update(key, JSON.stringify(newEntry));
57+
const updateOp = this.update(key, JSON.stringify(newEntry));
5658

5759
switch (newEntry.value?.kind) {
5860
case 'FETCH': {
@@ -70,6 +72,9 @@ export class CacheAdaptor {
7072
);
7173
}
7274
}
75+
76+
// Make sure the cache has been updated before returning
77+
await updateOp;
7378
}
7479

7580
/**
@@ -84,7 +89,12 @@ export class CacheAdaptor {
8489
{ softTags }: { softTags?: string[] },
8590
): Promise<CacheHandlerValue | null> {
8691
// Get entry from the cache.
87-
const entry = await this.retrieve(key);
92+
const entryPromise = this.retrieve(key);
93+
94+
// Start loading the tags manifest.
95+
const tagsManifestLoad = this.loadTagsManifest();
96+
97+
const entry = await entryPromise;
8898
if (!entry) return null;
8999

90100
let data: CacheHandlerValue;
@@ -97,8 +107,8 @@ export class CacheAdaptor {
97107

98108
switch (data.value?.kind) {
99109
case 'FETCH': {
100-
// Load the tags manifest.
101-
await this.loadTagsManifest();
110+
// Await for the tags manifest to end loading.
111+
await tagsManifestLoad;
102112

103113
// Check if the cache entry is stale or fresh based on the tags.
104114
const tags = getTagsFromEntry(data);
@@ -140,8 +150,29 @@ export class CacheAdaptor {
140150

141151
/**
142152
* Loads the tags manifest from the suspense cache.
153+
*
154+
* @param force Whether to force a reload of the tags manifest.
155+
*/
156+
public async loadTagsManifest(force = false): Promise<void> {
157+
// Load tags manifest if missing or refresh if forced.
158+
const shouldLoad = force || !this.tagsManifest;
159+
160+
if (!shouldLoad) {
161+
return;
162+
}
163+
164+
// If the tags manifest is not already being loaded, kickstart the retrieval.
165+
if (!this.tagsManifestPromise) {
166+
this.tagsManifestPromise = this.loadTagsManifestInternal();
167+
}
168+
169+
await this.tagsManifestPromise;
170+
}
171+
172+
/**
173+
* Internal method to load the tags manifest from the suspense cache.
143174
*/
144-
public async loadTagsManifest(): Promise<void> {
175+
private async loadTagsManifestInternal(): Promise<void> {
145176
try {
146177
const rawManifest = await this.retrieve(this.tagsManifestKey);
147178
if (rawManifest) {
@@ -152,6 +183,7 @@ export class CacheAdaptor {
152183
}
153184

154185
this.tagsManifest ??= { version: 1, items: {} };
186+
this.tagsManifestPromise = undefined;
155187
}
156188

157189
/**
@@ -174,7 +206,7 @@ export class CacheAdaptor {
174206
tags: string[],
175207
{ cacheKey, revalidatedAt }: { cacheKey?: string; revalidatedAt?: number },
176208
): Promise<void> {
177-
await this.loadTagsManifest();
209+
await this.loadTagsManifest(true);
178210

179211
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
180212
const tagsManifest = this.tagsManifest!;

0 commit comments

Comments
 (0)