Skip to content

Commit

Permalink
Remove client only dynamic chunks from edge bundle (#56761)
Browse files Browse the repository at this point in the history
### Issue

In the client components world, when you're using `next/dynamic` with `ssr: false` to split chunks in pages of edge runtime, you could get the dynamic imported module still bundled in the server bundle for edge runtime. This could easily hit the bundle limit on edge runtime if you're loading a large size of non-SSR module.

This is caused by the whole chunk is still being included when we're creating the client entry. Since the client entry is imported eagerily, webpack will bundle all the modules under it, unless it's explicitly marked not being included.

### Fix

For client components, SSR rendering layer of bundle, non-SSR `next/dynamic` calls, we're transform the result of `dynamic()` call from to conditional import the dynamic loaded module.

From
```js
dynamic(() => import(...))
```
To
```js
dynamic(() => {
  require.resolveWeak(...)
}, { ssr: false })
```

This will only be applied to SSR layer bundle client components non-SSR `next/dynamic` calls and only when webpack is bundling since turbopack doesn't need this. In this way, the server side will be stripped but it can still enter the module graph since we need to traverse if there's SA in client modules with using webpack API `require.resolveWeak`. And for client side bundle will still include the actual module.

Close NEXT-1703
  • Loading branch information
huozhi authored Nov 16, 2023
1 parent b017261 commit d6d6d56
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 22 deletions.
2 changes: 1 addition & 1 deletion packages/next-swc/crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ where
Some(config) if config.truthy() => match config {
// Always enable the Server Components mode for both
// server and client layers.
react_server_components::Config::WithOptions(_) => true,
react_server_components::Config::WithOptions(config) => config.is_react_server_layer,
_ => false,
},
_ => false,
Expand Down
22 changes: 21 additions & 1 deletion packages/next-swc/crates/core/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ fn next_dynamic_fixture(input: PathBuf) {
fn app_dir_next_dynamic_fixture(input: PathBuf) {
let output_dev = input.parent().unwrap().join("output-dev.js");
let output_prod = input.parent().unwrap().join("output-prod.js");
let output_server = input.parent().unwrap().join("output-server.js");
let output_server: PathBuf = input.parent().unwrap().join("output-server.js");
let output_server_client_layer = input
.parent()
.unwrap()
.join("output-server-client-layer.js");
test_fixture(
syntax(),
&|_tr| {
Expand Down Expand Up @@ -161,6 +165,22 @@ fn app_dir_next_dynamic_fixture(input: PathBuf) {
&output_server,
Default::default(),
);
test_fixture(
syntax(),
&|_tr| {
next_dynamic(
false,
true,
false,
NextDynamicMode::Webpack,
FileName::Real(PathBuf::from("/some-project/src/some-file.js")),
Some("/some-project/src".into()),
)
},
&input,
&output_server_client_layer,
Default::default(),
);
}

#[fixture("tests/fixture/ssg/**/input.js")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import dynamic from 'next/dynamic';
export const NextDynamicNoSSRServerComponent = dynamic(async ()=>{
typeof require.resolveWeak !== "undefined" && require.resolveWeak("../text-dynamic-no-ssr-server");
}, {
loadableGenerated: {
modules: [
"some-file.js -> " + "../text-dynamic-no-ssr-server"
]
},
ssr: false
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import dynamic from 'next/dynamic';
export const NextDynamicNoSSRServerComponent = dynamic(null, {
export const NextDynamicNoSSRServerComponent = dynamic(async ()=>{
typeof require.resolveWeak !== "undefined" && require.resolveWeak("../text-dynamic-no-ssr-server");
}, {
loadableGenerated: {
modules: [
"some-file.js -> " + "../text-dynamic-no-ssr-server"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hell
},
loading: ()=><p>...</p>
});
const DynamicClientOnlyComponent = dynamic(null, {
const DynamicClientOnlyComponent = dynamic(async ()=>{
typeof require.resolveWeak !== "undefined" && require.resolveWeak("../components/hello");
}, {
loadableGenerated: {
modules: [
"some-file.js -> " + "../components/hello"
]
},
ssr: false
});
const DynamicClientOnlyComponentWithSuspense = dynamic(()=>import('../components/hello'), {
const DynamicClientOnlyComponentWithSuspense = dynamic(async ()=>{
typeof require.resolveWeak !== "undefined" && require.resolveWeak("../components/hello");
}, {
loadableGenerated: {
modules: [
"some-file.js -> " + "../components/hello"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(null, {
const DynamicComponent = dynamic(async ()=>{
typeof require.resolveWeak !== "undefined" && require.resolveWeak("./components/hello");
}, {
loadableGenerated: {
modules: [
"some-file.js -> " + "./components/hello"
Expand Down
102 changes: 88 additions & 14 deletions packages/next-swc/crates/next-transform-dynamic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ use swc_core::{
common::{errors::HANDLER, FileName, Span, DUMMY_SP},
ecma::{
ast::{
ArrayLit, ArrowExpr, BlockStmtOrExpr, Bool, CallExpr, Callee, Expr, ExprOrSpread,
ExprStmt, Id, Ident, ImportDecl, ImportDefaultSpecifier, ImportNamedSpecifier,
ImportSpecifier, KeyValueProp, Lit, ModuleDecl, ModuleItem, Null, ObjectLit, Prop,
PropName, PropOrSpread, Stmt, Str, Tpl,
op, ArrayLit, ArrowExpr, BinExpr, BlockStmt, BlockStmtOrExpr, Bool, CallExpr, Callee,
Expr, ExprOrSpread, ExprStmt, Id, Ident, ImportDecl, ImportDefaultSpecifier,
ImportNamedSpecifier, ImportSpecifier, KeyValueProp, Lit, ModuleDecl, ModuleItem,
ObjectLit, Prop, PropName, PropOrSpread, Stmt, Str, Tpl, UnaryExpr, UnaryOp,
},
utils::{private_ident, ExprFactory},
utils::{private_ident, quote_ident, ExprFactory},
visit::{Fold, FoldWith},
},
quote,
Expand Down Expand Up @@ -236,12 +236,12 @@ impl Fold for NextDynamicPatcher {
rel_filename(self.pages_dir.as_deref(), &self.filename)
)
.into(),
right: Expr = dynamically_imported_specifier.into(),
right: Expr = dynamically_imported_specifier.clone().into(),
))
} else {
webpack_options(quote!(
"require.resolveWeak($id)" as Expr,
id: Expr = dynamically_imported_specifier.into()
id: Expr = dynamically_imported_specifier.clone().into()
))
}
}
Expand All @@ -259,7 +259,7 @@ impl Fold for NextDynamicPatcher {
imports.push(TurbopackImport::DevelopmentTransition {
id_ident: id_ident.clone(),
chunks_ident: chunks_ident.clone(),
specifier: dynamically_imported_specifier,
specifier: dynamically_imported_specifier.clone(),
});

// On the server, the key needs to be serialized because it
Expand All @@ -280,7 +280,7 @@ impl Fold for NextDynamicPatcher {
(true, false) => {
imports.push(TurbopackImport::DevelopmentId {
id_ident: id_ident.clone(),
specifier: dynamically_imported_specifier,
specifier: dynamically_imported_specifier.clone(),
});

// On the client, we only need the target module ID, which
Expand Down Expand Up @@ -365,11 +365,54 @@ impl Fold for NextDynamicPatcher {
}
}

// Also don't strip the `loader` argument for server components (both
// server/client layers), since they're aliased to a
// React.lazy implementation.
if has_ssr_false && self.is_server_compiler && !self.is_react_server_layer {
expr.args[0] = Lit::Null(Null { span: DUMMY_SP }).as_arg();
if has_ssr_false
&& self.is_server_compiler
&& !self.is_react_server_layer
// Only use `require.resolveWebpack` to decouple modules for webpack,
// turbopack doesn't need this
&& self.state == NextDynamicPatcherState::Webpack
{
// if it's server components SSR layer
// Transform 1st argument `expr.args[0]` aka the module loader from:
// dynamic(() => import('./client-mod'), { ssr: false }))`
// into:
// dynamic(async () => {
// require.resolveWeak('./client-mod')
// }, { ssr: false }))`

let require_resolve_weak_expr = Expr::Call(CallExpr {
span: DUMMY_SP,
callee: quote_ident!("require.resolveWeak").as_callee(),
args: vec![ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: dynamically_imported_specifier.clone().into(),
raw: None,
}))),
}],
type_args: Default::default(),
});

let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
span: DUMMY_SP,
params: vec![],
body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
span: DUMMY_SP,
stmts: vec![Stmt::Expr(ExprStmt {
span: DUMMY_SP,
expr: Box::new(exec_expr_when_resolve_weak_available(
&require_resolve_weak_expr,
)),
})],
})),
is_async: true,
is_generator: false,
type_params: None,
return_type: None,
});

expr.args[0] = side_effect_free_loader_arg.as_arg();
}

let second_arg = ExprOrSpread {
Expand Down Expand Up @@ -562,6 +605,37 @@ impl NextDynamicPatcher {
}
}

fn exec_expr_when_resolve_weak_available(expr: &Expr) -> Expr {
let undefined_str_literal = Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: "undefined".into(),
raw: None,
}));

let typeof_expr = Expr::Unary(UnaryExpr {
span: DUMMY_SP,
op: UnaryOp::TypeOf, // 'typeof' operator
arg: Box::new(Expr::Ident(Ident {
span: DUMMY_SP,
sym: quote_ident!("require.resolveWeak").sym,
optional: false,
})),
});

// typeof require.resolveWeak !== 'undefined' && <expression>
Expr::Bin(BinExpr {
span: DUMMY_SP,
left: Box::new(Expr::Bin(BinExpr {
span: DUMMY_SP,
op: op!("!=="),
left: Box::new(typeof_expr),
right: Box::new(undefined_str_literal),
})),
op: op!("&&"),
right: Box::new(expr.clone()),
})
}

fn rel_filename(base: Option<&Path>, file: &FileName) -> String {
let base = match base {
Some(v) => v,
Expand Down
14 changes: 14 additions & 0 deletions test/e2e/app-dir/actions/app-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,20 @@ createNextDescribe(
}, '1')
})

it('should support next/dynamic with ssr: false (edge)', async () => {
const browser = await next.browser('/dynamic-csr/edge')

await check(() => {
return browser.elementByCss('button').text()
}, '0')

await browser.elementByCss('button').click()

await check(() => {
return browser.elementByCss('button').text()
}, '1')
})

it('should only submit action once when resubmitting an action after navigation', async () => {
let requestCount = 0

Expand Down
20 changes: 20 additions & 0 deletions test/e2e/app-dir/actions/app/dynamic-csr/edge/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(
() => import('../csr').then((mod) => mod.CSR),
{
ssr: false,
}
)

export default function Client() {
return (
<div>
<DynamicComponent />
</div>
)
}

export const runtime = 'edge'
4 changes: 3 additions & 1 deletion test/e2e/app-dir/dynamic/app/chunk-loading/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

'use client'

function noop() {}

export default function Page() {
import('./comp').then((m) => {
console.log(m)
noop(m)
})
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export { default } from '../dynamic-import'

export const runtime = 'edge'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client'

export { default } from '../dynamic-import'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import dynamic from 'next/dynamic'

const DynamicSSRFalse = dynamic(() => import('./ssr-false-module'), {
ssr: false,
})

export default function page() {
return (
<div>
<DynamicSSRFalse />
<p id="content">dynamic-mixed-ssr-false</p>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Client from './ssr-false-client'
import Server from './ssr-false-server'

export default function Comp() {
return (
<>
<Client />
<Server />
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export default function Comp() {
return <p id="ssr-false-client-module">ssr-false-client-module-text</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Comp() {
return <p id="ssr-false-server-module">ssr-false-server-module-text</p>
}
Loading

0 comments on commit d6d6d56

Please sign in to comment.