Skip to content

fix(netlify): enable code-splitting for the netlify-edge preset - #4525

Open
JakeChampion wants to merge 1 commit into
nitrojs:mainfrom
JakeChampion:fix/netlify-edge-code-splitting
Open

fix(netlify): enable code-splitting for the netlify-edge preset#4525
JakeChampion wants to merge 1 commit into
nitrojs:mainfrom
JakeChampion:fix/netlify-edge-code-splitting

Conversation

@JakeChampion

Copy link
Copy Markdown

What

Set inlineDynamicImports: false in the netlify-edge preset's rollup output config, so dynamic imports stay dynamic and the build emits chunks, matching what the cloudflare-pages / cloudflare-module presets already do.

Why

netlify-edge extends base-worker, which sets inlineDynamicImports: true with the comment "iffe does not support code-splitting". This preset overrides the output format to esm, where that rationale no longer applies, but it kept inheriting the inlining.

The consequence: every intended-lazy dynamic import gets hoisted into the single entry chunk, and because ESM top-level code runs at module-link time, it executes on every isolate cold start. Nitro's own lazy route handlers all load eagerly, and libraries that deliberately defer expensive init behind import() lose that laziness entirely. The concrete case that surfaced this: nuxt-og-image lazy-loads its resvg WASM binding, but with the inlined build the WebAssembly.instantiate call runs unconditionally at boot, so every cold start pays WASM compilation even for requests that never render an OG image (fix on their side: nuxt-modules/og-image#673 - both changes are independently useful).

Does the platform support this?

Yes - verified in production:

  1. Chunked output survives the deploy pipeline. Netlify bundles the full module graph of the generated edge function, including emitted chunks (both statically imported ones and import() targets; the template-literal specifiers Rollup emits are followed too).
  2. Runtime import() works. Built Nitro's own test/fixture with this change (.netlify/edge-functions/server/ gets server.js plus _chunks/, _routes/, _libs/ etc.), deployed it to a production Netlify site, and exercised it:
    • regular API routes (every route handler is a lazy chunk): work
    • /wasm/dynamic-import and /wasm/static-import: return 2+3=5
    • a separate minimal test confirmed a dynamically imported sibling chunk containing a top-level await WebAssembly.instantiate(...) works, is not evaluated at isolate boot, and behaves correctly on the very first request served by a freshly created isolate
  3. pnpm lint and pnpm typecheck pass.

Effect on cold starts

With this change the fixture's entry drops from one monolithic file to a small entry + chunks, and deferred work (route handlers, WASM compilation) executes on first use instead of at boot. For deployments with heavy lazily-imported dependencies this removes the dominant cold-start cost.

netlify-edge extends base-worker, which sets inlineDynamicImports: true
with the rationale that iife output cannot code-split. This preset
overrides the output format to esm, where that rationale no longer
applies, but kept inheriting the inlining. As a result intended-lazy
dynamic imports (lazy route handlers, deferred WASM init in libraries)
are hoisted into the entry chunk and their top-level code runs on every
isolate cold start.

Netlify bundles the full module graph of the generated function,
including emitted chunks, and dynamic import of those chunks works at
runtime. Set inlineDynamicImports: false (matching the cloudflare
presets) so lazily-imported work stays off the cold start path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sibh1BhDY97CPafumYmJyU
@JakeChampion
JakeChampion requested a review from pi0 as a code owner August 11, 2026 12:41
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

@JakeChampion is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95c2b76a-edf8-4bbb-be9b-93fbc610b239

📥 Commits

Reviewing files that changed from the base of the PR and between 16ff280 and 5e64a66.

📒 Files selected for processing (1)
  • src/presets/netlify/preset.ts

📝 Walkthrough

Walkthrough

The Netlify edge preset now sets Rollup’s inlineDynamicImports option to false. The emitted ESM server bundle retains dynamically importable chunks.

Changes

Netlify dynamic import bundling

Layer / File(s) Summary
Configure dynamic import output
src/presets/netlify/preset.ts
The Netlify edge Rollup configuration disables dynamic-import inlining.

Estimated code review effort: 2 (Simple) | ~5 minutes

Possibly related PRs

  • nitrojs/nitro#4436: Both changes modify inlineDynamicImports behavior for code-split output.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits syntax with the fix type, netlify scope, and a change-focused description.
Description check ✅ Passed The description clearly explains the configuration change, its rationale, platform verification, and expected cold-start impact.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pi0

pi0 commented Aug 11, 2026

Copy link
Copy Markdown
Member

Please make a minimal deployment of a Nitro v3 app (current branch) with same setup to proof it works.

@JakeChampion

Copy link
Copy Markdown
Author

Live deployment - https://nitro-pr-4525-code-splitting.netlify.app

Setup

  • nitro packed from this branch (pnpm pack at 5e64a66), installed via file: dependency.
  • Config is just the preset:
import { defineConfig } from "nitro";

export default defineConfig({
  preset: "netlify-edge",
  serverDir: "server",
  compatibilityDate: "latest",
});
  • Routes: / (status), /add (dynamically imports a sibling module that does a top-level await WebAssembly.instantiate(...), mirroring the resvg/og-image pattern), and the same /wasm/dynamic-import + /wasm/static-import routes as test/fixture (via unwasm/examples/sum.wasm).
  • A boot plugin records bootAt and a per-isolate id; the lazy wasm module records its own evaluation time on globalThis, so laziness is observable from responses.

Build output (nitro build)

.netlify/edge-functions/server/_chunks/wasm-add.mjs
.netlify/edge-functions/server/_libs/{h3+rou3+srvx,hookable,srvx,unwasm,_}.mjs
.netlify/edge-functions/server/_routes/add.mjs
.netlify/edge-functions/server/_routes/index.mjs
.netlify/edge-functions/server/_routes/wasm/dynamic_import.mjs
.netlify/edge-functions/server/_routes/wasm/static_import.mjs
.netlify/edge-functions/server/server.js   (entry, 3.4 kB)

Every route handler and the wasm module are import()-ed chunks. Netlify's deploy bundling picks up the full graph (all chunks are present in the generated bundle).

Proof from the live site, fresh isolate

$ curl https://nitro-pr-4525-code-splitting.netlify.app/
{"message":"nitro v3 netlify-edge code-splitting demo (nitrojs/nitro#4525)",
 "isolate":"155f7186-...","bootAt":1786455542653,"now":1786455542659,
 "wasmAddChunkEvaluatedAt":null}          # <- isolate booted, wasm chunk NOT evaluated

$ curl https://nitro-pr-4525-code-splitting.netlify.app/add
{"result":"2+3=5","chunkEvaluatedAt":1786455542921,
 "bootAt":1786455542653,"isolate":"155f7186-..."}   # <- chunk evaluated on first use, wasm works

$ curl https://nitro-pr-4525-code-splitting.netlify.app/
{...,"isolate":"155f7186-...","wasmAddChunkEvaluatedAt":1786455542921}  # <- same isolate, evaluated once

$ curl https://nitro-pr-4525-code-splitting.netlify.app/wasm/dynamic-import
2+3=5
$ curl https://nitro-pr-4525-code-splitting.netlify.app/wasm/static-import
2+3=5
$ curl -o /dev/null -w "%{http_code}" https://nitro-pr-4525-code-splitting.netlify.app/robots.txt
200

So on the same isolate: boot happens without evaluating the lazy chunk (wasmAddChunkEvaluatedAt: null on first request), the chunk's top-level WebAssembly.instantiate runs on the first /add request and works correctly, and stays cached afterwards. Both fixture wasm routes work as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants