๐Ÿ“ฆ southpolesteve / sentry-opennext-wasm-repro

Repro: @sentry/nextjs embeds a 3.3MB WASM module in Cloudflare Worker bundles via OpenNext

โ˜… 0 stars โ‘‚ 0 forks ๐Ÿ‘ 0 watching
๐Ÿ“ฅ Clone https://github.com/southpolesteve/sentry-opennext-wasm-repro.git
HTTPS git clone https://github.com/southpolesteve/sentry-opennext-wasm-repro.git
SSH git clone git@github.com:southpolesteve/sentry-opennext-wasm-repro.git
CLI gh repo clone southpolesteve/sentry-opennext-wasm-repro
Loading files...
๐Ÿ“„ README.md

Sentry + OpenNext WASM Bundle Repro

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.

The Problem

@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.

Why the 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:

  • Next.js (turbopack) builds first using the node condition
  • Turbopack bundles @sentry/node-core -> injectLoader.js -> tracing-hooks -> code-transformer (with inline WASM) into server chunks
  • By the time OpenNext's esbuild runs, the WASM is already baked into the pre-built Next.js chunks
  • OpenNext's esbuild just re-bundles these chunks -- it can't un-inline the WASM

Bundle Sizes (Before vs After Workaround)

MetricBefore (no workaround)After (with workaround)
handler.mjs11.0 MB4.5 MB
wrangler upload16.0 MB / 4.8 MB gzip9.2 MB / 2.2 MB gzip
WASM presentYes (3.3MB, duplicated)No
Under Workers 10MB limitNoYes
Sentry functionalN/A (can't deploy)Yes
For comparison, when esbuild resolves @sentry/nextjs directly (outside Next.js):

Bundle with --conditions=workerd     516 KB  (NO wasm)
Bundle with --conditions=node        4.3 MB  (has wasm)

Workaround (Works Today)

Two changes in next.config.ts plus a small stub file:

1. next.config.ts

import 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...
});

2. stubs/empty-code-transformer.js

module.exports.create = function create() {
  return {
    getTransformer() {
      return undefined;
    },
  };
};

Why both settings are needed

  • 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.
The alias alone is sufficient, but both together ensure the module is excluded at every stage of the build pipeline.

Does the workaround break anything?

No. The stubbed-out module (@apm-js-collab/code-transformer) is a build-time Node.js tool that:

  • Requires node:module (Module.prototype._compile) which doesn't exist in Workers
  • Is only used by the pino integration in @sentry/node-core, which calls addInstrumentationConfig()
  • That function is gated behind supportsEsmLoaderHooks(), which returns false in CJS mode (the OpenNext bundle is CJS)
  • Even if reached, the stub returns { getTransformer() { return undefined } }, which means "no matching instrumentation" -- the same behavior as having no instrumentations configured
Verified: the worker boots, Sentry initializes (hasSentry: true), and API routes work correctly.

Why serverExternalPackages alone doesn't work

Tested 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.

Where Upstream Fixes Should Happen

  • Sentry (@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 file
  • Next.js/turbopack: Respect workerd conditions when the build target is known to be non-Node
  • OpenNext: Add a built-in alias for @apm-js-collab/code-transformer -> empty module (they already do this for @ampproject/toolbox-optimizer, edge-runtime, etc.)

Reproduce

# 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}

Tech Stack

  • next@16.1.6
  • @sentry/nextjs@^10.38.0
  • @opennextjs/cloudflare@^1.16.5
  • wrangler@4.65.0