-
Notifications
You must be signed in to change notification settings - Fork 27k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Omit unused args when calling
"use cache"
functions (#72506)
Using the information byte in a server reference ID, as introduced in #71463, we can omit unused arguments when calling a `"use cache"` function. This enables us, for example, to use a cached function in conjunction with `useActionState` when the function does not need the previous state, as shown below: ```ts 'use cache' export async function getCachedRandomValue() { return Math.random() } ``` ```tsx 'use client' import { useActionState } from 'react' import { getCachedRandomValue } from './cached' export default function Page() { const [result, formAction] = useActionState(getCachedRandomValue, null) return ( <form action={formAction}> <p>{result}</p> <button>Submit</button> </form> ) } ``` Previously, in this use case, a new value would have been rendered for every click. This is because React passes the previous state and the form data as arguments into the action function. These two arguments are then included in the cache key, resulting in a new cache key being generated for every invocation. _Note: This optimization is currently based purely on the function signature. It does not yet detect when a param is declared, but not used in the function body. It's also not yet applied for server actions._
- Loading branch information
1 parent
d81a9f7
commit 912d42b
Showing
6 changed files
with
326 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
193 changes: 193 additions & 0 deletions
193
packages/next/src/client/components/router-reducer/reducers/server-reference-info.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import { | ||
type ServerReferenceInfo, | ||
extractInfoFromServerReferenceId, | ||
omitUnusedArgs, | ||
} from './server-reference-info' | ||
|
||
describe('extractInfoFromServerReferenceId', () => { | ||
test('should parse id with typeBit 0, no args used, no restArgs', () => { | ||
const id = '00' // 0b00000000 | ||
|
||
const expected: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [false, false, false, false, false, false], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(extractInfoFromServerReferenceId(id)).toEqual(expected) | ||
}) | ||
|
||
test('should parse id with typeBit 1, all args used, restArgs true', () => { | ||
const id = 'ff' // 0b11111111 | ||
|
||
const expected: ServerReferenceInfo = { | ||
type: 'use-cache', | ||
usedArgs: [true, true, true, true, true, true], | ||
hasRestArgs: true, | ||
} | ||
|
||
expect(extractInfoFromServerReferenceId(id)).toEqual(expected) | ||
}) | ||
|
||
test('should parse id with typeBit 0, argMask 0b101010, restArgs false', () => { | ||
const id = '54' // 0b01010100 | ||
|
||
const expected: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [true, false, true, false, true, false], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(extractInfoFromServerReferenceId(id)).toEqual(expected) | ||
}) | ||
|
||
test('should parse id with typeBit 1, argMask 0b000101, restArgs true', () => { | ||
const id = '8b' // 0b10001011 | ||
|
||
const expected: ServerReferenceInfo = { | ||
type: 'use-cache', | ||
usedArgs: [false, false, false, true, false, true], | ||
hasRestArgs: true, | ||
} | ||
|
||
expect(extractInfoFromServerReferenceId(id)).toEqual(expected) | ||
}) | ||
}) | ||
|
||
describe('omitUnusedArgs', () => { | ||
test('should return empty array when no args are used and no restArgs', () => { | ||
const args = ['arg1', 'arg2', 'arg3'] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [false, false, false, false, false, false], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual([]) | ||
}) | ||
|
||
test('should return all args when all args are used and has restArgs', () => { | ||
const args = [ | ||
'arg1', | ||
'arg2', | ||
'arg3', | ||
'arg4', | ||
'arg5', | ||
'arg6', | ||
'restArg1', | ||
'restArg2', | ||
] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'use-cache', | ||
usedArgs: [true, true, true, true, true, true], | ||
hasRestArgs: true, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual(args) | ||
}) | ||
|
||
test('should filter args when some args are used and no restArgs', () => { | ||
const args = ['arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6'] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [true, false, true, false, true, false], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual([ | ||
'arg1', | ||
undefined, | ||
'arg3', | ||
undefined, | ||
'arg5', | ||
undefined, | ||
]) | ||
}) | ||
|
||
test('should include restArgs when hasRestArgs is true', () => { | ||
const args = [ | ||
'arg1', | ||
'arg2', | ||
'arg3', | ||
'arg4', | ||
'arg5', | ||
'arg6', | ||
'restArg1', | ||
'restArg2', | ||
] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'use-cache', | ||
usedArgs: [false, false, false, true, false, true], | ||
hasRestArgs: true, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual([ | ||
undefined, | ||
undefined, | ||
undefined, | ||
'arg4', | ||
undefined, | ||
'arg6', | ||
'restArg1', | ||
'restArg2', | ||
]) | ||
}) | ||
|
||
test('should not include extra args when hasRestArgs is false', () => { | ||
const args = [ | ||
'arg1', | ||
'arg2', | ||
'arg3', | ||
'arg4', | ||
'arg5', | ||
'arg6', | ||
'extraArg1', | ||
'extraArg2', | ||
] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [true, true, true, true, true, true], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual([ | ||
'arg1', | ||
'arg2', | ||
'arg3', | ||
'arg4', | ||
'arg5', | ||
'arg6', | ||
undefined, | ||
undefined, | ||
]) | ||
}) | ||
|
||
test('should handle args array shorter than 6 elements', () => { | ||
const args = ['arg1', 'arg2', 'arg3'] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [true, false, true, false, false, false], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual(['arg1', undefined, 'arg3']) | ||
}) | ||
|
||
test('should handle empty args array', () => { | ||
const args: unknown[] = [] | ||
|
||
const info: ServerReferenceInfo = { | ||
type: 'server-action', | ||
usedArgs: [false, false, false, false, false, false], | ||
hasRestArgs: false, | ||
} | ||
|
||
expect(omitUnusedArgs(args, info)).toEqual(args) | ||
}) | ||
}) |
78 changes: 78 additions & 0 deletions
78
packages/next/src/client/components/router-reducer/reducers/server-reference-info.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
export interface ServerReferenceInfo { | ||
type: 'server-action' | 'use-cache' | ||
usedArgs: [boolean, boolean, boolean, boolean, boolean, boolean] | ||
hasRestArgs: boolean | ||
} | ||
|
||
/** | ||
* Extracts info about the server reference for the given server reference ID by | ||
* parsing the first byte of the hex-encoded ID. | ||
* | ||
* ``` | ||
* Bit positions: [7] [6] [5] [4] [3] [2] [1] [0] | ||
* Bits: typeBit argMask restArgs | ||
* ``` | ||
* | ||
* If the `typeBit` is `1` the server reference represents a `"use cache"` | ||
* function, otherwise a server action. | ||
* | ||
* The `argMask` encodes whether the function uses the argument at the | ||
* respective position. | ||
* | ||
* The `restArgs` bit indicates whether the function uses a rest parameter. It's | ||
* also set to 1 if the function has more than 6 args. | ||
* | ||
* @param id hex-encoded server reference ID | ||
*/ | ||
export function extractInfoFromServerReferenceId( | ||
id: string | ||
): ServerReferenceInfo { | ||
const infoByte = parseInt(id.slice(0, 2), 16) | ||
const typeBit = (infoByte >> 7) & 0x1 | ||
const argMask = (infoByte >> 1) & 0x3f | ||
const restArgs = infoByte & 0x1 | ||
const usedArgs = Array(6) | ||
|
||
for (let index = 0; index < 6; index++) { | ||
const bitPosition = 5 - index | ||
const bit = (argMask >> bitPosition) & 0x1 | ||
usedArgs[index] = bit === 1 | ||
} | ||
|
||
return { | ||
type: typeBit === 1 ? 'use-cache' : 'server-action', | ||
usedArgs: usedArgs as [ | ||
boolean, | ||
boolean, | ||
boolean, | ||
boolean, | ||
boolean, | ||
boolean, | ||
], | ||
hasRestArgs: restArgs === 1, | ||
} | ||
} | ||
|
||
/** | ||
* Creates a sparse array containing only the used arguments based on the | ||
* provided action info. | ||
*/ | ||
export function omitUnusedArgs( | ||
args: unknown[], | ||
info: ServerReferenceInfo | ||
): unknown[] { | ||
const filteredArgs = new Array(args.length) | ||
|
||
for (let index = 0; index < args.length; index++) { | ||
if ( | ||
(index < 6 && info.usedArgs[index]) || | ||
// This assumes that the server reference info byte has the restArgs bit | ||
// set to 1 if there are more than 6 args. | ||
(index >= 6 && info.hasRestArgs) | ||
) { | ||
filteredArgs[index] = args[index] | ||
} | ||
} | ||
|
||
return filteredArgs | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
'use cache' | ||
|
||
export async function getRandomValue() { | ||
const v = Math.random() | ||
console.log(v) | ||
return v | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
'use client' | ||
|
||
import { useActionState } from 'react' | ||
import { getRandomValue } from './cached' | ||
|
||
export default function Page() { | ||
const [result, formAction, isPending] = useActionState(getRandomValue, -1) | ||
|
||
return ( | ||
<form action={formAction}> | ||
<button id="submit-button">Submit</button> | ||
<p>{isPending ? 'loading...' : result}</p> | ||
</form> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters