Skip to content

Commit d155ba9

Browse files
[turbopack] Lazily compile dynamic imports in development (client side) (#97203)
## Summary Defer compiling client-side dynamic import targets in Turbopack development until the browser requests their manifest chunk. This avoids compiling untouched dynamic imports while preserving server-side imports, CSS loading, Server Actions, source maps, and Fast Refresh behavior. One caveat on this is next/dynamic still does some eagerness. I looked a bit at changing things. But it breaks some of the guarantees there and decided to leave it off the table. --------- Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
1 parent c5741d5 commit d155ba9

71 files changed

Lines changed: 1763 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/next-api/src/app.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use turbopack_core::{
5252
asset::AssetContent,
5353
chunk::{
5454
ChunkGroupResult, ChunkingContext, ChunkingContextExt, EvaluatableAsset, EvaluatableAssets,
55-
availability_info::AvailabilityInfo,
55+
HmrChunkListSource, availability_info::AvailabilityInfo,
5656
},
5757
file_source::FileSource,
5858
ident::{AssetIdent, Layer},
@@ -1434,7 +1434,11 @@ impl AppEndpoint {
14341434
let client_reference_chunks =
14351435
get_client_references_chunks_for_hmr(*client_references_chunks);
14361436
client_chunking_context
1437-
.hmr_chunk_list(client_components_chunks_ident, client_reference_chunks)
1437+
.hmr_chunk_list(
1438+
client_components_chunks_ident,
1439+
client_reference_chunks,
1440+
HmrChunkListSource::Entry,
1441+
)
14381442
.await?
14391443
.iter()
14401444
.copied()

‎crates/next-api/src/project.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ use turbopack_core::{
8787
NotFoundVersion, OptionVersionedContent, Update, Version, VersionState, VersionedContent,
8888
},
8989
};
90+
use turbopack_ecmascript::async_chunk::proxy::{
91+
activation_key_from_chunk_path, lazy_compilation_state,
92+
};
9093
#[cfg(all(feature = "process_pool", not(target_family = "wasm")))]
9194
use turbopack_node::child_process_backend;
9295
use turbopack_node::execution_context::ExecutionContext;
@@ -456,6 +459,17 @@ fn project_operation(project: ResolvedVc<ProjectContainer>) -> Vc<Project> {
456459
project.project()
457460
}
458461

462+
/// Activates the lazy dynamic import that `chunk_path` names, returning whether it named one. The
463+
/// caller has to rebuild the owning entrypoint before serving the request.
464+
#[turbo_tasks::function(operation, root)]
465+
pub async fn activate_lazy_chunk_operation(chunk_path: RcStr) -> Result<Vc<bool>> {
466+
let Some(key) = activation_key_from_chunk_path(&chunk_path) else {
467+
return Ok(Vc::cell(false));
468+
};
469+
lazy_compilation_state(key).await?.activate();
470+
Ok(Vc::cell(true))
471+
}
472+
459473
#[turbo_tasks::function(operation, root)]
460474
fn project_fs_operation(project: ResolvedVc<Project>) -> Vc<DiskFileSystem> {
461475
project.project_fs()
@@ -1599,6 +1613,9 @@ impl Project {
15991613
.turbo_nested_async_chunking(self.next_mode(), true),
16001614
shared_runtime: self.next_config().turbo_shared_runtime(self.next_mode()),
16011615
per_page_module_graph: self.per_page_module_graph(),
1616+
lazy_dynamic_imports: self
1617+
.next_config()
1618+
.turbopack_lazy_dynamic_imports(*self.next_mode().await?),
16021619
debug_ids: self.next_config().turbopack_debug_ids(),
16031620
worker_asset_prefix: self.next_config().turbopack_worker_asset_prefix(),
16041621
should_use_absolute_url_references: self.next_config().inline_css(),

‎crates/next-core/src/next_client/context.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ pub async fn get_client_module_options_context(
379379
mangle_export_names: *next_config.turbopack_mangle_export_names(mode).await?,
380380
cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?,
381381
cross_module_constants: *next_config.turbopack_cross_module_constants().await?,
382+
lazy_compilation: *next_config
383+
.turbopack_lazy_dynamic_imports(*next_mode)
384+
.await?,
382385
preset_env_config,
383386
..Default::default()
384387
},
@@ -411,6 +414,7 @@ pub async fn get_client_module_options_context(
411414
enable_typeof_window_inlining: None,
412415
// Ignore e.g. import(`${url}`) requests in node_modules.
413416
ignore_dynamic_requests: true,
417+
lazy_compilation: false,
414418
// Don't inject core-js polyfills into node_modules — only user code
415419
// should be processed by preset_env's usage/entry mode.
416420
preset_env_config: None,
@@ -433,6 +437,7 @@ pub async fn get_client_module_options_context(
433437
enable_jsx: Some(JsxTransformOptions::default().resolved_cell()),
434438
// Don't inject core-js polyfills into framework internals.
435439
preset_env_config: None,
440+
lazy_compilation: false,
436441
..module_options_context.ecmascript.clone()
437442
},
438443
enable_postcss_transform: None,
@@ -492,6 +497,7 @@ pub struct ClientChunkingContextOptions {
492497
pub nested_async_chunking: Vc<bool>,
493498
pub shared_runtime: Vc<bool>,
494499
pub per_page_module_graph: Vc<bool>,
500+
pub lazy_dynamic_imports: Vc<bool>,
495501
pub debug_ids: Vc<bool>,
496502
pub worker_asset_prefix: Vc<Option<RcStr>>,
497503
pub should_use_absolute_url_references: Vc<bool>,
@@ -541,6 +547,7 @@ pub async fn get_client_chunking_context(
541547
nested_async_chunking,
542548
shared_runtime,
543549
per_page_module_graph,
550+
lazy_dynamic_imports,
544551
debug_ids,
545552
worker_asset_prefix,
546553
should_use_absolute_url_references,
@@ -618,7 +625,9 @@ pub async fn get_client_chunking_context(
618625
builder = builder
619626
.hot_module_replacement()
620627
.source_map_source_type(SourceMapSourceType::AbsoluteFileUri)
621-
.dynamic_chunk_content_loading(true);
628+
.dynamic_chunk_content_loading(true)
629+
// A manifest chunk keeps a lazily compiled import's URL stable across activation.
630+
.manifest_chunks(*lazy_dynamic_imports.await?);
622631
} else {
623632
builder = builder
624633
.chunking_config(

‎crates/next-core/src/next_config.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,9 @@ pub struct ExperimentalConfig {
14161416
turbopack_worker_asset_prefix: Option<RcStr>,
14171417
turbopack_client_side_nested_async_chunking: Option<bool>,
14181418
turbopack_server_side_nested_async_chunking: Option<bool>,
1419+
/// Compile client dynamic import targets when their runtime proxy is first activated.
1420+
/// Development only.
1421+
turbopack_lazy_dynamic_imports: Option<bool>,
14191422
turbopack_import_type_bytes: Option<bool>,
14201423
/// Disable automatic configuration of the sass loader.
14211424
#[serde(default)]
@@ -2741,6 +2744,17 @@ impl NextConfig {
27412744
}))
27422745
}
27432746

2747+
#[turbo_tasks::function]
2748+
pub async fn turbopack_lazy_dynamic_imports(&self, next_mode: NextMode) -> Vc<bool> {
2749+
Vc::cell(
2750+
next_mode.is_development()
2751+
&& self
2752+
.experimental
2753+
.turbopack_lazy_dynamic_imports
2754+
.unwrap_or(false),
2755+
)
2756+
}
2757+
27442758
#[turbo_tasks::function]
27452759
pub async fn turbopack_import_type_bytes(&self) -> Vc<bool> {
27462760
Vc::cell(

‎crates/next-napi-bindings/src/next_api/project.rs‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use next_api::{
3030
},
3131
project::{
3232
DebugBuildPaths, DefineEnv, DraftModeOptions, PartialProjectOptions, Project,
33-
ProjectContainer, ProjectOptions, WatchOptions,
33+
ProjectContainer, ProjectOptions, WatchOptions, activate_lazy_chunk_operation,
3434
},
3535
project_asset_hashes_manifest::immutable_hashes_manifest_asset_if_enabled,
3636
route::{Endpoint, EndpointGroupKey, Route},
@@ -747,6 +747,23 @@ pub async fn project_update(
747747
.await
748748
}
749749

750+
#[tracing::instrument(level = "info", name = "activate lazy chunk", skip_all)]
751+
#[napi]
752+
pub async fn project_activate_lazy_chunk(
753+
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: &External<ProjectInstance>,
754+
chunk_path: RcStr,
755+
) -> napi::Result<bool> {
756+
let ctx = &project.turbopack_ctx;
757+
ctx.turbo_tasks()
758+
.run(async move {
759+
Ok(*activate_lazy_chunk_operation(chunk_path)
760+
.read_strongly_consistent()
761+
.await?)
762+
})
763+
.await
764+
.map_err(|error| napi::Error::from_reason(PrettyPrintError(&error.into()).to_string()))
765+
}
766+
750767
/// Invalidates the filesystem cache so that it will be deleted next time that a turbopack project
751768
/// is created with filesystem cache enabled.
752769
#[napi]

‎packages/next/src/build/swc/generated-native.d.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,11 @@ export declare function parse(
567567
signal?: AbortSignal | undefined | null
568568
): Promise<string>
569569

570+
export declare function projectActivateLazyChunk(
571+
project: { __napiType: 'Project' },
572+
chunkPath: RcStr
573+
): Promise<boolean>
574+
570575
export declare function projectClientHmrChunkNamesSubscribe(
571576
project: { __napiType: 'Project' },
572577
func: (err: Error, value: TurbopackResult<HmrChunkNames>) => void

‎packages/next/src/build/swc/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,10 @@ function bindingToApi(
696696
)
697697
}
698698

699+
async activateLazyChunk(chunkPath: string): Promise<boolean> {
700+
return binding.projectActivateLazyChunk(this._nativeProject, chunkPath)
701+
}
702+
699703
async writeAnalyzeData(
700704
appDirOnly: boolean
701705
): Promise<TurbopackResult<void>> {

‎packages/next/src/build/swc/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ export interface UpdateInfo {
317317
export interface Project {
318318
update(options: Partial<ProjectOptions>): Promise<void>
319319

320+
activateLazyChunk(chunkPath: string): Promise<boolean>
321+
320322
writeAnalyzeData(appDirOnly: boolean): Promise<TurbopackResult<void>>
321323

322324
getAllCompilationIssues(): Promise<TurbopackResult<void>>

‎packages/next/src/server/config-schema.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ export const experimentalSchema = {
416416
turbopackWorkerAssetPrefix: z.string().optional(),
417417
turbopackClientSideNestedAsyncChunking: z.boolean().optional(),
418418
turbopackServerSideNestedAsyncChunking: z.boolean().optional(),
419+
turbopackLazyDynamicImports: z.boolean().optional(),
419420
turbopackImportTypeBytes: z.boolean().optional(),
420421
turbopackUseBuiltinBabel: z.boolean().optional(),
421422
turbopackUseBuiltinSass: z.boolean().optional(),

‎packages/next/src/server/config-shared.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,13 @@ export interface ExperimentalConfig {
948948
*/
949949
turbopackServerSideNestedAsyncChunking?: boolean
950950

951+
/**
952+
* Compile client dynamic import targets when they are first used in development.
953+
*
954+
* Defaults to `false`.
955+
*/
956+
turbopackLazyDynamicImports?: boolean
957+
951958
/**
952959
* Enable filesystem cache for the turbopack dev server.
953960
*

0 commit comments

Comments
 (0)