Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/next-core/src/next_client/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ pub async fn get_client_module_options_context(
infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?,
cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?,
cross_module_constants: *next_config.turbopack_cross_module_constants().await?,
preset_env_config,
..Default::default()
},
Expand Down
11 changes: 11 additions & 0 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,8 @@ pub struct ExperimentalConfig {
turbopack_cjs_tree_shaking: Option<bool>,
/// Enable scope hoisting of static CommonJS modules. Defaults to false.
turbopack_cjs_scope_hoisting: Option<bool>,
/// Enable cross-module constant inlining. Defaults to false.
turbopack_cross_module_constants: Option<bool>,
/// Devtool option for the segment explorer.
devtool_segment_explorer: Option<bool>,
/// Whether to report inlined system environment variables as warnings or errors.
Expand Down Expand Up @@ -2576,6 +2578,15 @@ impl NextConfig {
)
}

#[turbo_tasks::function]
pub fn turbopack_cross_module_constants(&self) -> Vc<bool> {
Vc::cell(
self.experimental
.turbopack_cross_module_constants
.unwrap_or(false),
)
}

#[turbo_tasks::function]
pub fn turbopack_plugin_runtime_strategy(&self) -> Vc<TurbopackPluginRuntimeStrategy> {
#[cfg(feature = "process_pool")]
Expand Down
1 change: 1 addition & 0 deletions crates/next-core/src/next_server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ pub async fn get_server_module_options_context(
infer_module_side_effects: *next_config.turbopack_infer_module_side_effects().await?,
cjs_tree_shaking: *next_config.turbopack_cjs_tree_shaking().await?,
cjs_scope_hoisting: *next_config.turbopack_cjs_scope_hoisting().await?,
cross_module_constants: *next_config.turbopack_cross_module_constants().await?,
..Default::default()
},
execution_context: Some(execution_context),
Expand Down
14 changes: 10 additions & 4 deletions docs/01-app/02-guides/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ When a browser requests a page, two streams work together during the initial pag

### The HTML stream

React's server renderer produces progressive HTML chunks. The static parts of your page (layouts, navigation, Suspense fallbacks) render first and are sent immediately. When an async [Server Component](/docs/app/glossary#server-component) resolves, React streams its completed HTML along with inline `<script>` tags: one that swaps the fallback DOM node with the new content, and another carrying the [component payload](#the-component-payload) so React can later hydrate it. The browser executes the swap instantly, without waiting for the page's JavaScript bundle to load or hydration to complete. This is what the user _sees_: the page painting progressively, section by section.
React's server renderer produces progressive HTML chunks. The static parts of your page (layouts, navigation, Suspense fallbacks) render first and are sent immediately. When a `<Suspense>` boundary's content is ready, for example when an async [Server Component](/docs/app/glossary#server-component) resolves, React streams its completed HTML along with inline `<script>` tags: one that swaps the fallback DOM node with the new content, and another carrying the [component payload](#the-component-payload) so React can later hydrate it. The browser executes the swap instantly, without waiting for the page's JavaScript bundle to load or hydration to complete. This is what the user _sees_: the page painting progressively, section by section.

### The component payload

Expand Down Expand Up @@ -579,11 +579,17 @@ Without streaming, the server waits for all data before sending any HTML, so TTF

### LCP (Largest Contentful Paint)

If your LCP element (a hero image, a main heading, a product photo) is inside a Suspense boundary, it can't paint until that boundary resolves. To keep LCP fast:
If your LCP element (a hero image, a main heading, a product photo) is inside a Suspense boundary, it can't paint until that boundary's content is swapped in. The element then depends on the work the server does to render it, not on your initial server response time. Revealing it costs something on the client too, because React streams a small inline script alongside the boundary's HTML and the content only appears once that script runs.

Data fetching is not the only reason a boundary delays your LCP element. React also holds back a large boundary, because sending its HTML takes time. See [what activates a Suspense boundary](https://react.dev/reference/react/Suspense#what-activates-a-suspense-boundary).

> **Good to know:** As a rule of thumb, if there's a Suspense boundary, React might use it. Under a slow network or a busy CPU, concurrent rendering can fall back to it even when you didn't expect it. Adding a boundary means accepting that, so don't add one you don't need.

To keep LCP fast:

- Keep LCP elements **outside** or **above** Suspense boundaries so they render as part of the static shell.
- Use the [`preload`](/docs/app/api-reference/components/image#preload) prop on `next/image` for LCP images. This injects a `<link rel="preload">` into the `<head>`, so the browser starts fetching the image from the very first chunk, before the `<img>` tag even appears in the HTML.
- For non-image LCP elements (text, headings), make sure they are not wrapped in a Suspense boundary that depends on slow data.
- Use the [`preload`](/docs/app/api-reference/components/image#preload) prop on `next/image` for LCP images. This injects a `<link rel="preload">` into the `<head>`, so the browser starts fetching the image from the very first chunk, before the `<img>` tag even appears in the HTML. It controls when the image is fetched, not when it paints. An image inside a boundary still waits for the swap.
- For non-image LCP elements (text, headings), render them outside Suspense boundaries.

### CLS (Cumulative Layout Shift)

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ export const experimentalSchema = {
turbopackInferModuleSideEffects: z.boolean().optional(),
turbopackCjsTreeShaking: z.boolean().optional(),
turbopackCjsScopeHoisting: z.boolean().optional(),
turbopackCrossModuleConstants: z.boolean().optional(),
turbopackServerFastRefresh: z.boolean().optional(),
optimizePackageImports: z.array(z.string()).optional(),
optimizeServerReact: z.boolean().optional(),
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,14 @@ export interface ExperimentalConfig {
*/
turbopackCjsScopeHoisting?: boolean

/**
* Enable cross-module constant inlining in Turbopack. Constants exported from other
* modules are inlined at their use sites, which enables dead code elimination.
*
* Defaults to `false`
*/
turbopackCrossModuleConstants?: boolean

/**
* Set this to `false` to disable the automatic configuration of the babel loader when a Babel
* configuration file is present. This option is enabled by default.
Expand Down
7 changes: 6 additions & 1 deletion packages/next/src/server/use-cache/use-cache-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1412,8 +1412,13 @@ async function generateCacheEntryImpl(
)

clearTimeout(timer)
const didTimeout = timeoutAbortController.signal.aborted
if (dynamicAccessAbortSignal) {
// Release React's listener from the composite signal.
timeoutAbortController.abort()
}

if (timeoutAbortController.signal.aborted) {
if (didTimeout) {
// When the timeout is reached we always error the stream. Even for
// fallback shell prerenders we don't want to return a hanging promise,
// which would allow the function to become a dynamic hole. Because that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,23 @@ import path from 'path'
* - NOT crash with a `TurbopackInternalError` / "FATAL" log
* - recover once `node_modules/next` is restored
*/
const describeMaybe = process.env.NEXT_SKIP_ISOLATE ? describe.skip : describe
const describeMaybe =
process.env.NEXT_SKIP_ISOLATE || !process.env.IS_TURBOPACK_TEST
? describe.skip
: describe

describeMaybe('concurrent-install', () => {
const { next, isTurbopack } = nextTestSetup({
files: __dirname,
})

const itTurbopack = isTurbopack ? it : it.skip

async function getNextPath(): Promise<string> {
async function getNextPath(next): Promise<string> {
const nextPath = path.join(next.testDir, 'node_modules', 'next')
// sanity check
await fs.lstat(nextPath)
return nextPath
}

async function moveNextAside(): Promise<{ original: string; stash: string }> {
const original = await getNextPath()
async function moveNextAside(
next
): Promise<{ original: string; stash: string }> {
const original = await getNextPath(next)
const stash = `${original}.stash-${Date.now()}`
await fs.rename(original, stash)
return { original, stash }
Expand All @@ -47,13 +46,16 @@ describeMaybe('concurrent-install', () => {
await fs.rename(stash, original)
}

itTurbopack(
'does not crash when node_modules/next is moved mid-session',
async () => {
describe('does not crash when node_modules/next is moved mid-session', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('works', async () => {
await next.browser('/')

const getOutput = next.getCliOutputFromHere()
const stashInfo = await moveNextAside()
const stashInfo = await moveNextAside(next)
try {
// Force a recompile while next is missing. Not strickly necessary, but important to ensure
// we do recover with the new content eventually
Expand Down Expand Up @@ -86,16 +88,19 @@ describeMaybe('concurrent-install', () => {
'FATAL: An unexpected Turbopack error occurred'
)
expect(getOutput()).not.toContain('TurbopackInternalError')
}
)
})
})

itTurbopack(
'surfaces a friendly issue when node_modules/next is missing',
async () => {
describe('surfaces a friendly issue when node_modules/next is missing', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('works', async () => {
await next.browser('/')

const getOutput = next.getCliOutputFromHere()
const stashInfo = await moveNextAside()
const stashInfo = await moveNextAside(next)
try {
await next.patchFile(
'app/page.tsx',
Expand Down Expand Up @@ -127,17 +132,20 @@ describeMaybe('concurrent-install', () => {
} finally {
await restoreNext(stashInfo)
}
}
)
})
})

itTurbopack(
'does not crash when navigating to an uncompiled route while node_modules/next is missing',
async () => {
describe('does not crash when navigating to an uncompiled route while node_modules/next is missing', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('works', async () => {
// Compile `/` so the harness has at least one warm chunk.
await next.browser('/')

const getOutput = next.getCliOutputFromHere()
const stashInfo = await moveNextAside()
const stashInfo = await moveNextAside(next)
try {
// Navigating to `/late-route` (never compiled in this session) forces
// a fresh `hmr_version_state` evaluation for that chunk. That path
Expand Down Expand Up @@ -175,6 +183,6 @@ describeMaybe('concurrent-install', () => {
15000,
500
)
}
)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -71,62 +71,53 @@ describe('app-dir - error-on-next-codemod-comment', () => {
})

it('should error with inline comment as well', async () => {
let originFileContent
await next.patchFile('app/page.tsx', (code) => {
originFileContent = code
return code.replace(
'// @next-codemod-error remove jsx of next line',
'/* @next-codemod-error remove jsx of next line */'
)
})

const browser = await next.browser('/')

await waitForRedbox(browser)

// Recover the original file content
await next.patchFile('app/page.tsx', originFileContent)
await next.patchFile(
'app/page.tsx',
(code) =>
code.replace(
'// @next-codemod-error remove jsx of next line',
'/* @next-codemod-error remove jsx of next line */'
),
async () => {
const browser = await next.browser('/')
await retry(async () => {
await waitForRedbox(browser)
}, 10000)
}
)
})

it('should disappear the error when you rre the codemod comment', async () => {
const browser = await next.browser('/')

await waitForRedbox(browser)

let originFileContent
await next.patchFile('app/page.tsx', (code) => {
originFileContent = code
return code.replace(
'// @next-codemod-error remove jsx of next line',
''
)
})

await retry(async () => {
await waitForNoRedbox(browser)
})

// Recover the original file content
await next.patchFile('app/page.tsx', originFileContent)
await next.patchFile(
'app/page.tsx',
(code) =>
code.replace('// @next-codemod-error remove jsx of next line', ''),
async () => {
await retry(async () => {
await waitForNoRedbox(browser)
}, 10000)
}
)
})

it('should disappear the error when you replace with bypass comment', async () => {
const browser = await next.browser('/')

await waitForRedbox(browser)

let originFileContent
await next.patchFile('app/page.tsx', (code) => {
originFileContent = code
return code.replace('@next-codemod-error', '@next-codemod-bypass')
})

await retry(async () => {
await waitForNoRedbox(browser)
})

// Recover the original file content
await next.patchFile('app/page.tsx', originFileContent)
await next.patchFile(
'app/page.tsx',
(code) => code.replace('@next-codemod-error', '@next-codemod-bypass'),
async () => {
await retry(async () => {
await waitForNoRedbox(browser)
}, 10000)
}
)
})
} else {
it('should fail the build with next build', async () => {
Expand Down
Loading
Loading