vercel/next.js · #98589
Add React Compiler memoization preservation option
crates/next-core/src/next_config.rs23 + / 4 −
@@ -32,7 +32,8 @@ use turbopack_core::{ resolve::ResolveAliasMap, }; use turbopack_ecmascript::transform::{- OptionReactCompilerCompilationMode, ReactCompilerCompilationMode, ReactCompilerTarget,+ OptionReactCompilerTransformOptions, ReactCompilerCompilationMode, ReactCompilerTarget,+ ReactCompilerTransformOptions, }; use turbopack_ecmascript_plugins::transform::{ emotion::EmotionTransformConfig, relay::RelayConfig,@@ -766,12 +767,22 @@ pub enum ReactCompilerPanicThreshold { pub struct ReactCompilerOptions { #[serde(default)] pub compilation_mode: ReactCompilerCompilationMode,+ #[serde(default, skip_serializing_if = "Option::is_none")]+ pub environment: Option<ReactCompilerEnvironmentOptions>, #[serde(default)] pub panic_threshold: ReactCompilerPanicThreshold, #[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")] pub target: Option<ReactCompilerTarget>, } +#[turbo_tasks::value(shared, operation)]+#[derive(Clone, Debug, Default, Serialize, Deserialize)]+#[serde(rename_all = "camelCase")]+pub struct ReactCompilerEnvironmentOptions {+ #[serde(default, skip_serializing_if = "Option::is_none")]+ pub enable_preserve_existing_memoization_guarantees: Option<bool>,+}+ #[derive(Clone, Debug, PartialEq, Deserialize, NonLocalValue, OperationValue, Encode, Decode)] #[serde(untagged)] pub enum ReactCompilerOptionsOrBoolean {@@ -2082,17 +2093,25 @@ impl NextConfig { /// Returns compilation mode when both `reactCompiler` and `turbopackRustReactCompiler` are set; /// `None` otherwise. #[turbo_tasks::function]- pub fn rust_react_compiler(&self) -> Vc<OptionReactCompilerCompilationMode> {+ pub fn rust_react_compiler(&self) -> Vc<OptionReactCompilerTransformOptions> { let use_rust = self .experimental .turbopack_rust_react_compiler .unwrap_or(false); let mode = match (use_rust, &self.react_compiler) { (true, Some(ReactCompilerOptionsOrBoolean::Boolean(true))) => {- Some(ReactCompilerCompilationMode::Infer)+ Some(ReactCompilerTransformOptions::default()) } (true, Some(ReactCompilerOptionsOrBoolean::Option(opts))) => {- Some(opts.compilation_mode)+ Some(ReactCompilerTransformOptions {+ compilation_mode: opts.compilation_mode,+ enable_preserve_existing_memoization_guarantees: opts+ .environment+ .as_ref()+ .and_then(|environment| {+ environment.enable_preserve_existing_memoization_guarantees+ }),+ }) } _ => None, };crates/next-core/src/next_shared/webpack_rules/babel.rs30 + / 3 −
@@ -19,7 +19,7 @@ use turbopack_ecmascript::transform::{ReactCompilerCompilationMode, ReactCompile use turbopack_node::transforms::webpack::WebpackLoaderItem; use crate::{- next_config::{NextConfig, ReactCompilerOptions},+ next_config::{NextConfig, ReactCompilerOptions, ReactCompilerPanicThreshold}, next_import_map::try_get_next_package, next_shared::webpack_rules::{ ManuallyConfiguredBuiltinLoaderIssue, WebpackLoaderBuiltinCondition,@@ -171,20 +171,47 @@ pub async fn get_babel_loader_rules( #[serde(rename_all = "camelCase")] struct EnvironmentOptions { enable_name_anonymous_functions: bool,+ #[serde(skip_serializing_if = "Option::is_none")]+ enable_preserve_existing_memoization_guarantees: Option<bool>, } #[derive(Serialize)] struct ResolvedOptions<'a> { #[serde(flatten)]- base: &'a ReactCompilerOptions,+ base: ReactCompilerOptionsWithoutEnvironment<'a>, environment: EnvironmentOptions, } + #[derive(Serialize)]+ #[serde(rename_all = "camelCase")]+ struct ReactCompilerOptionsWithoutEnvironment<'a> {+ compilation_mode: ReactCompilerCompilationMode,+ panic_threshold: &'a ReactCompilerPanicThreshold,+ #[serde(skip_serializing_if = "Option::is_none")]+ target: Option<ReactCompilerTarget>,+ }++ impl<'a> From<&'a ReactCompilerOptions> for ReactCompilerOptionsWithoutEnvironment<'a> {+ fn from(options: &'a ReactCompilerOptions) -> Self {+ Self {+ compilation_mode: options.compilation_mode,+ panic_threshold: &options.panic_threshold,+ target: options.target,+ }+ }+ }+ let resolved_options = ResolvedOptions {- base: &react_compiler_options_with_target,+ base: (&react_compiler_options_with_target).into(), environment: EnvironmentOptions { enable_name_anonymous_functions: builtin_conditions .contains(&WebpackLoaderBuiltinCondition::Development),+ enable_preserve_existing_memoization_guarantees: react_compiler_options_with_target+ .environment+ .as_ref()+ .and_then(|environment| {+ environment.enable_preserve_existing_memoization_guarantees+ }), }, }; let react_compiler_plugins =docs/01-app/03-api-reference/05-config/01-next-config-js/reactCompiler.mdx33 + / 0 −
@@ -97,6 +97,39 @@ export default function Page() { > **Note:** You can also use the `"use no memo"` directive from React for the opposite effect, to opt-out a component or hook. +## Existing memoization guarantees++You can configure whether the compiler preserves existing memoization guarantees from `useMemo`, `useCallback`, and `React.memo`:++```ts filename="next.config.ts" switcher+import type { NextConfig } from 'next'++const nextConfig: NextConfig = {+ reactCompiler: {+ environment: {+ enablePreserveExistingMemoizationGuarantees: false,+ },+ },+}++export default nextConfig+```++```js filename="next.config.js" switcher+/** @type {import('next').NextConfig} */+const nextConfig = {+ reactCompiler: {+ environment: {+ enablePreserveExistingMemoizationGuarantees: false,+ },+ },+}++module.exports = nextConfig+```++When omitted, Next.js uses the default from the active React Compiler implementation. Because defaults may differ between compiler implementations and versions, set this option explicitly to ensure consistent behavior.+ ## Version History | Version | Changes |packages/next/src/build/get-babel-loader-config.ts14 + / 6 −
@@ -39,17 +39,25 @@ const getReactCompilerPlugins = ( // react/package.json not resolvable — skip target detection } - const environment: Pick<EnvironmentConfig, 'enableNameAnonymousFunctions'> = {- enableNameAnonymousFunctions: isDev,- } const options: ReactCompilerOptions = typeof maybeOptions === 'boolean' ? {} : maybeOptions- const compilerOptions: JSONValue = {- ...options,+ const { environment: userEnvironment, ...compilerOptions } = options+ const environment = {+ enableNameAnonymousFunctions: isDev,+ ...(userEnvironment?.enablePreserveExistingMemoizationGuarantees !==+ undefined+ ? {+ enablePreserveExistingMemoizationGuarantees:+ userEnvironment.enablePreserveExistingMemoizationGuarantees,+ }+ : {}),+ } satisfies Partial<EnvironmentConfig>+ const resolvedCompilerOptions: JSONValue = {+ ...compilerOptions, ...(target ? { target } : {}), environment, }- return [[getReactCompiler(), compilerOptions]]+ return [[getReactCompiler(), resolvedCompilerOptions]] } const getBabelLoader = (packages/next/src/server/config-schema.ts7 + / 0 −
@@ -859,6 +859,13 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() => z .object({ compilationMode: z.enum(['infer', 'annotation', 'all']).optional(),+ environment: z+ .object({+ enablePreserveExistingMemoizationGuarantees: z+ .boolean()+ .optional(),+ })+ .optional(), panicThreshold: z .enum(['none', 'critical_errors', 'all_errors']) .optional(),packages/next/src/server/config-shared.ts9 + / 0 −
@@ -359,6 +359,15 @@ export interface ReactCompilerOptions { * @see https://react.dev/reference/react-compiler/compilationMode */ compilationMode?: 'infer' | 'annotation' | 'all'+ environment?: {+ /**+ * Controls whether the React Compiler preserves existing memoization+ * guarantees from `useMemo`, `useCallback`, and `React.memo`.+ *+ * When omitted, the installed React Compiler's default is used.+ */+ enablePreserveExistingMemoizationGuarantees?: boolean+ } /** * Controls how the React Compiler handles errors during compilation. *turbopack/crates/turbopack-ecmascript/src/transform/mod.rs69 + / 16 −
@@ -92,11 +92,21 @@ pub enum EcmascriptInputTransform { use_define_for_class_fields: bool, }, ReactCompilerRust {- compilation_mode: ReactCompilerCompilationMode,+ options: ReactCompilerTransformOptions, target: ReactCompilerTarget, }, } +#[turbo_tasks::value(shared, operation)]+#[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]+#[serde(rename_all = "camelCase")]+pub struct ReactCompilerTransformOptions {+ #[serde(default)]+ pub compilation_mode: ReactCompilerCompilationMode,+ #[serde(default, skip_serializing_if = "Option::is_none")]+ pub enable_preserve_existing_memoization_guarantees: Option<bool>,+}+ #[turbo_tasks::value(shared, operation)] #[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")]@@ -118,7 +128,7 @@ impl ReactCompilerCompilationMode { } #[turbo_tasks::value(transparent)]-pub struct OptionReactCompilerCompilationMode(Option<ReactCompilerCompilationMode>);+pub struct OptionReactCompilerTransformOptions(Option<ReactCompilerTransformOptions>); #[turbo_tasks::value(shared, operation)] #[derive(Default, Debug, Clone, Copy, Hash, Serialize, Deserialize)]@@ -401,11 +411,8 @@ impl EcmascriptInputTransform { apply_transform(program, helpers, decorators(config)) }- EcmascriptInputTransform::ReactCompilerRust {- compilation_mode,- target,- } => {- apply_rust_react_compiler(program, ctx, helpers, *compilation_mode, *target).await?+ EcmascriptInputTransform::ReactCompilerRust { options, target } => {+ apply_rust_react_compiler(program, ctx, helpers, *options, *target).await? } EcmascriptInputTransform::Plugin(transform) => { // We cannot pass helpers to plugins, so we return them as is@@ -538,7 +545,7 @@ async fn apply_rust_react_compiler( program: &mut Program, ctx: &TransformContext<'_>, helpers: HelperData,- compilation_mode: ReactCompilerCompilationMode,+ options: ReactCompilerTransformOptions, target: ReactCompilerTarget, ) -> Result<HelperData> { let Program::Module(_) = program else {@@ -548,7 +555,7 @@ async fn apply_rust_react_compiler( // Avoid invoking the compiler when the selected mode cannot change this module. These checks // run on the SWC AST we already parsed, before converting it to the compiler AST. `All` mode // remains unconditional because every function is eligible.- if !should_run_rust_react_compiler(program, compilation_mode) {+ if !should_run_rust_react_compiler(program, options.compilation_mode) { return Ok(helpers); } @@ -559,7 +566,7 @@ async fn apply_rust_react_compiler( swc_ecma_react_compiler::SourceType::from_program(program), ctx.source_text, Some(&single_threaded_comments),- react_compiler_options(ctx, compilation_mode, target),+ react_compiler_options(ctx.node_env.as_str(), ctx.file_name_str, options, target), ); // TODO: Emit these diagnostics with an Info level once there's a way of adjusting log levels in@@ -585,18 +592,19 @@ async fn apply_rust_react_compiler( } fn react_compiler_options(- ctx: &TransformContext<'_>,- compilation_mode: ReactCompilerCompilationMode,+ node_env: &str,+ file_name: &str,+ options: ReactCompilerTransformOptions, target: ReactCompilerTarget, ) -> react_compiler::entrypoint::plugin_options::PluginOptions { use react_compiler::entrypoint::plugin_options::{CompilerTarget, PluginOptions}; - PluginOptions {+ let mut plugin_options = PluginOptions { should_compile: true, enable_reanimated: false,- is_dev: ctx.node_env != "production",- filename: Some(ctx.file_name_str.to_string()),- compilation_mode: compilation_mode.as_str().to_string(),+ is_dev: node_env != "production",+ filename: Some(file_name.to_string()),+ compilation_mode: options.compilation_mode.as_str().to_string(), panic_threshold: "none".to_string(), target: CompilerTarget::Version(target.as_str().to_string()), gating: None,@@ -611,7 +619,17 @@ fn react_compiler_options( source_code: None, profiling: false, debug: false,+ };+ if let Some(enable_preserve_existing_memoization_guarantees) =+ options.enable_preserve_existing_memoization_guarantees+ {+ plugin_options+ .environment+ .enable_preserve_existing_memoization_guarantees =+ enable_preserve_existing_memoization_guarantees; }++ plugin_options } fn apply_transform(program: &mut Program, helpers: HelperData, op: impl Pass) -> HelperData {@@ -690,6 +708,41 @@ mod react_compiler_tests { use super::*; + #[test]+ fn preserves_default_environment_without_explicit_option() {+ let options = react_compiler_options(+ "production",+ "input.js",+ ReactCompilerTransformOptions::default(),+ ReactCompilerTarget::React19,+ );++ assert!(+ options+ .environment+ .enable_preserve_existing_memoization_guarantees+ );+ }++ #[test]+ fn forwards_explicit_memoization_preservation_option() {+ let options = react_compiler_options(+ "production",+ "input.js",+ ReactCompilerTransformOptions {+ compilation_mode: ReactCompilerCompilationMode::Infer,+ enable_preserve_existing_memoization_guarantees: Some(false),+ },+ ReactCompilerTarget::React19,+ );++ assert!(+ !options+ .environment+ .enable_preserve_existing_memoization_guarantees+ );+ }+ fn parse_program(source: &str) -> Program { GLOBALS.set(&Default::default(), || { let cm = SourceMap::default();turbopack/crates/turbopack-tests/tests/snapshot.rs8 + / 4 −
@@ -56,7 +56,8 @@ use turbopack_core::{ }; use turbopack_ecmascript::{ AnalyzeMode, CustomTransformer, EcmascriptInputTransform, TransformPlugin,- chunk::EcmascriptChunkType, transform::ReactCompilerCompilationMode,+ chunk::EcmascriptChunkType,+ transform::{ReactCompilerCompilationMode, ReactCompilerTransformOptions}, }; use turbopack_ecmascript_plugins::transform::{ emotion::{EmotionTransformConfig, EmotionTransformer},@@ -429,9 +430,12 @@ async fn run_test_operation(resource: RcStr) -> Result<Vc<FileSystemPath>> { cjs_scope_hoisting: options.cjs_scope_hoisting, cross_module_constants: options.cross_module_constants, enable_exports_info_inlining: true,- enable_rust_react_compiler: options- .enable_rust_react_compiler- .then_some(ReactCompilerCompilationMode::Infer),+ enable_rust_react_compiler: options.enable_rust_react_compiler.then_some(+ ReactCompilerTransformOptions {+ compilation_mode: ReactCompilerCompilationMode::Infer,+ ..Default::default()+ },+ ), ..Default::default() }, environment: Some(env),turbopack/crates/turbopack/src/module_options/mod.rs2 + / 2 −
@@ -309,9 +309,9 @@ impl ModuleOptions { let mut ecma_preprocess = vec![]; let mut postprocess = vec![]; - if let Some(compilation_mode) = enable_rust_react_compiler {+ if let Some(options) = enable_rust_react_compiler { ecma_preprocess.push(EcmascriptInputTransform::ReactCompilerRust {- compilation_mode,+ options, target: rust_react_compiler_target, }); }turbopack/crates/turbopack/src/module_options/module_options_context.rs2 + / 2 −
@@ -16,7 +16,7 @@ use turbopack_core::{ use turbopack_ecmascript::{ AnalyzeMode, TypeofWindow, references::esm::UrlRewriteBehavior,- transform::{PresetEnvConfig, ReactCompilerCompilationMode, ReactCompilerTarget},+ transform::{PresetEnvConfig, ReactCompilerTarget, ReactCompilerTransformOptions}, }; pub use turbopack_mdx::MdxTransformOptions; use turbopack_node::{@@ -254,7 +254,7 @@ pub struct EcmascriptOptionsContext { // node_modules. pub enable_typeof_window_inlining: Option<TypeofWindow>, pub enable_jsx: Option<ResolvedVc<JsxTransformOptions>>,- pub enable_rust_react_compiler: Option<ReactCompilerCompilationMode>,+ pub enable_rust_react_compiler: Option<ReactCompilerTransformOptions>, pub rust_react_compiler_target: ReactCompilerTarget, /// Follow type references and resolve declaration files in additional to /// normal resolution.