Repro: @sentry/nextjs embeds a 3.3MB WASM module in Cloudflare Worker bundles via OpenNext
https://github.com/southpolesteve/sentry-opennext-wasm-repro.git
This repo demonstrates that @sentry/nextjs includes a ~3.3MB WASM module in the final Cloudflare Worker bundle when built via OpenNext, and provides a working workaround that eliminates it with no loss of functionality.
@sentry/nextjs has a dependency chain that pulls in a large WASM binary:
@sentry/nextjs (index.server.js -- the "node" entry point)
โโโ @sentry/node
โโโ @sentry/node-core
โโโ injectLoader.js (top-level require)
โโโ @apm-js-collab/tracing-hooks (top-level require)
โโโ @apm-js-collab/code-transformer
โโโ orchestrion_js.js -- 3.3MB base64-encoded WASM (2.47MB decoded)
The WASM module is SWC (Rust JS/TS parser) compiled to WebAssembly. It's used at Node.js build/load time to inject diagnostics_channel.TracingChannel calls for automatic library instrumentation (monkey-patching Module.prototype._compile). It has zero purpose at runtime in a Worker.
workerd Export Condition Doesn't Help@sentry/nextjs has export conditions in its package.json:
{
".": {
"workerd": "./build/esm/edge/index.js", // uses @sentry/vercel-edge, NO wasm
"node": "./build/cjs/index.server.js" // uses @sentry/node, HAS wasm
}
}
OpenNext passes conditions: ["workerd"] to its esbuild step. However, this doesn't help because:
node condition@sentry/node-core -> injectLoader.js -> tracing-hooks -> code-transformer (with inline WASM) into server chunks| Metric | Before (no workaround) | After (with workaround) |
|---|---|---|
| handler.mjs | 11.0 MB | 4.5 MB |
| wrangler upload | 16.0 MB / 4.8 MB gzip | 9.2 MB / 2.2 MB gzip |
| WASM present | Yes (3.3MB, duplicated) | No |
| Under Workers 10MB limit | No | Yes |
| Sentry functional | N/A (can't deploy) | Yes |
@sentry/nextjs directly (outside Next.js):
Bundle with --conditions=workerd 516 KB (NO wasm)
Bundle with --conditions=node 4.3 MB (has wasm)
Two changes in next.config.ts plus a small stub file:
next.config.tsimport type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";
const nextConfig: NextConfig = {
serverExternalPackages: ["@apm-js-collab/code-transformer"],
turbopack: {
resolveAlias: {
"@apm-js-collab/code-transformer": "./stubs/empty-code-transformer.js",
},
},
};
export default withSentryConfig(nextConfig, {
// your sentry options...
});
stubs/empty-code-transformer.jsmodule.exports.create = function create() {
return {
getTransformer() {
return undefined;
},
};
};
turbopack.resolveAlias -- Prevents Next.js/turbopack from bundling the 3.3MB WASM into .next/server/chunks/ during next build. This is the critical one.serverExternalPackages -- Tells Next.js to keep the require external rather than inlining the module. Belt-and-suspenders with the alias.No. The stubbed-out module (@apm-js-collab/code-transformer) is a build-time Node.js tool that:
node:module (Module.prototype._compile) which doesn't exist in Workers@sentry/node-core, which calls addInstrumentationConfig()supportsEsmLoaderHooks(), which returns false in CJS mode (the OpenNext bundle is CJS){ getTransformer() { return undefined } }, which means "no matching instrumentation" -- the same behavior as having no instrumentations configuredhasSentry: true), and API routes work correctly.
serverExternalPackages alone doesn't workTested this -- serverExternalPackages: ["@apm-js-collab/code-transformer"] prevents turbopack from inlining the WASM into Next.js chunks, but OpenNext's esbuild step re-resolves the external require and bundles the real node_modules/@apm-js-collab/code-transformer (with its 3.3MB WASM) right back into handler.mjs. The turbopack alias swaps the module to a stub so there's nothing for esbuild to pull back in.
@sentry/node-core): Add workerd/worker export conditions that exclude injectLoader.js and its WASM dependency chain@apm-js-collab/code-transformer: Ship the WASM as a separate .wasm file instead of base64-inlining it in a 3.3MB JS fileworkerd conditions when the build target is known to be non-Node@apm-js-collab/code-transformer -> empty module (they already do this for @ampproject/toolbox-optimizer, edge-runtime, etc.)# See the problem (without workaround -- revert next.config.ts to remove alias)
npm install
npm run build:worker
npm run check-wasm
# See the fix (with workaround -- as committed)
npm install
npm run build:worker
npm run check-wasm # should show no WASM
npx wrangler dev # should boot and serve requests
curl http://localhost:8787/api/test # should return {"hasSentry": true}
next@16.1.6@sentry/nextjs@^10.38.0@opennextjs/cloudflare@^1.16.5wrangler@4.65.0