@metamask/connect-evm bundle is around 2× larger than @metamask/sdk due to unrelated transitive dependencies
Package: @metamask/connect-evm@0.9.1
Summary
Migrating from @metamask/sdk to @metamask/connect-evm produces a nearly 2× increase in minified chunk size with no change in runtime behavior. I created a minimal project following only the instructions at https://docs.metamask.io/metamask-connect/evm/quickstart/wagmi/ for both sdk and connect-evm. The difference in chunks was as follows:
| Package |
Chunk size (minified) |
@metamask/sdk |
542.12 kB |
@metamask/connect-evm |
1026.73 kB |
Root cause
@metamask/connect-evm is architecturally a thin EIP-1193 adapter over @metamask/connect-multichain; every session, transport, and RPC call is delegated through createMultichainClient, and MetamaskConnectEVM wraps a MultichainCore instance directly. This means the entire connect-multichain dependency graph is transitively bundled into EVM-only consumers, including any multichain-specific code that is never exercised in that context.
The coupling runs deeper than the top-level class. The EIP1193Provider implementation imports EventEmitter and RPCInvokeMethodErr directly from @metamask/connect-multichain. These are generic primitives, a base event emitter and an RPC error class, that have no inherent relationship to multichain functionality. Their presence inside connect-multichain is a strong signal that the shared foundation has never been properly separated from protocol-specific code.
This is further evidenced by EIP1193Provider accessing MultichainCore's options property via an as any type assertion, with an accompanying TODO noting it requires refactoring. connect-evm is reaching into connect-multichain internals through an unofficial channel because the public API surface of connect-multichain was never designed to support connect-evm being layered on top of it. This is an architectural boundary problem, not just a dependency graph one.
Additionally, @metamask/chain-agnostic-permission is imported solely for parseScopeString, which is called once in disconnect() to check whether a scope's namespace prefix equals "eip155", a trivial string operation that does not justify pulling in an entire package.
Finally, @metamask/analytics is deeply integrated into the request lifecycle, with analytics.track(...) called on every request, success, rejection, and failure path throughout the class. This makes it impossible to tree-shake without a build-time module alias override, forcing consumers who do not want telemetry to maintain external workarounds (e.g., a Vite/Rollup resolve.alias stub).
Expected behaviour
A package named connect-evm should have a dependency graph constrained to EVM connectivity primitives only. The shared transport, session management, storage adapters, event emitter, and RPC error types that are currently embedded inside connect-multichain should be extracted into a connect-core (or equivalent) base package with a stable, intentionally designed public API. Both connect-evm and connect-multichain would then depend on connect-core, allowing EVM-only consumers to avoid bundling any multichain-specific code entirely, and eliminating the need for as any casts across the abstraction boundary.
Suggested remediation
- Extract shared primitives:
EventEmitter, RPCInvokeMethodErr, provider transport, session lifecycle, storage adapters, and CAIP scope utilities into a @metamask/connect-core package with an explicit, stable public API.
- Rebuild
@metamask/connect-evm on top of @metamask/connect-core rather than @metamask/connect-multichain, so the multichain-specific dependency graph is not transitively included and the as any internal access can be replaced with first-class API surface.
- Remove
@metamask/chain-agnostic-permission as a dependency of connect-evm. The single usage (parseScopeString for an eip155 namespace prefix check) should either be inlined as a string operation or provided by connect-core as a lightweight utility.
- Declare
@metamask/analytics as an optional peer dependency, or place it behind a side-effect-free export condition, so consumers are not forced to ship telemetry code or maintain build-tool workarounds to suppress it.
Impact
This bundle bloat directly degrades Lighthouse/Core Web Vitals scores (TTI, TBT), increases cold-start parse and compile time in V8/SpiderMonkey, and imposes unnecessary bandwidth costs on end users, particularly on mobile and constrained connections.
EDIT
The drastic change in bundle size originally reported was not only related to MetaMask, but also changes to Rolldown in Vite 8.x. After reverting to 3.5.0 and rebuilding I'm now seeing the metamask bundle at around 540kB. To isolate more properly, I created minimal reproduction projects building with vite 8.0.4, and the difference is closer to 2x. All of the rest above is still valid, and 2x increase is unreasonable. The full list of dependencies:
"dependencies": {
"@metamask/connect-evm": "^0.9.1",
"@tanstack/react-query": "^5.97.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"viem": "~2.47.11",
"wagmi": "3.6.1"
},
vs
"dependencies": {
"@metamask/sdk": "^0.34.0",
"@tanstack/react-query": "^5.97.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"viem": "~2.47.11",
"wagmi": "3.5.0"
},
@metamask/connect-evmbundle is around 2× larger than@metamask/sdkdue to unrelated transitive dependenciesPackage:
@metamask/connect-evm@0.9.1Summary
Migrating from
@metamask/sdkto@metamask/connect-evmproduces a nearly 2× increase in minified chunk size with no change in runtime behavior. I created a minimal project following only the instructions at https://docs.metamask.io/metamask-connect/evm/quickstart/wagmi/ for both sdk and connect-evm. The difference in chunks was as follows:@metamask/sdk@metamask/connect-evmRoot cause
@metamask/connect-evmis architecturally a thin EIP-1193 adapter over@metamask/connect-multichain; every session, transport, and RPC call is delegated throughcreateMultichainClient, andMetamaskConnectEVMwraps aMultichainCoreinstance directly. This means the entireconnect-multichaindependency graph is transitively bundled into EVM-only consumers, including any multichain-specific code that is never exercised in that context.The coupling runs deeper than the top-level class. The
EIP1193Providerimplementation importsEventEmitterandRPCInvokeMethodErrdirectly from@metamask/connect-multichain. These are generic primitives, a base event emitter and an RPC error class, that have no inherent relationship to multichain functionality. Their presence insideconnect-multichainis a strong signal that the shared foundation has never been properly separated from protocol-specific code.This is further evidenced by
EIP1193ProvideraccessingMultichainCore'soptionsproperty via anas anytype assertion, with an accompanyingTODOnoting it requires refactoring.connect-evmis reaching intoconnect-multichaininternals through an unofficial channel because the public API surface ofconnect-multichainwas never designed to supportconnect-evmbeing layered on top of it. This is an architectural boundary problem, not just a dependency graph one.Additionally,
@metamask/chain-agnostic-permissionis imported solely forparseScopeString, which is called once indisconnect()to check whether a scope's namespace prefix equals"eip155", a trivial string operation that does not justify pulling in an entire package.Finally,
@metamask/analyticsis deeply integrated into the request lifecycle, withanalytics.track(...)called on every request, success, rejection, and failure path throughout the class. This makes it impossible to tree-shake without a build-time module alias override, forcing consumers who do not want telemetry to maintain external workarounds (e.g., a Vite/Rollupresolve.aliasstub).Expected behaviour
A package named
connect-evmshould have a dependency graph constrained to EVM connectivity primitives only. The shared transport, session management, storage adapters, event emitter, and RPC error types that are currently embedded insideconnect-multichainshould be extracted into aconnect-core(or equivalent) base package with a stable, intentionally designed public API. Bothconnect-evmandconnect-multichainwould then depend onconnect-core, allowing EVM-only consumers to avoid bundling any multichain-specific code entirely, and eliminating the need foras anycasts across the abstraction boundary.Suggested remediation
EventEmitter,RPCInvokeMethodErr, provider transport, session lifecycle, storage adapters, and CAIP scope utilities into a@metamask/connect-corepackage with an explicit, stable public API.@metamask/connect-evmon top of@metamask/connect-corerather than@metamask/connect-multichain, so the multichain-specific dependency graph is not transitively included and theas anyinternal access can be replaced with first-class API surface.@metamask/chain-agnostic-permissionas a dependency ofconnect-evm. The single usage (parseScopeStringfor aneip155namespace prefix check) should either be inlined as a string operation or provided byconnect-coreas a lightweight utility.@metamask/analyticsas an optional peer dependency, or place it behind a side-effect-free export condition, so consumers are not forced to ship telemetry code or maintain build-tool workarounds to suppress it.Impact
This bundle bloat directly degrades Lighthouse/Core Web Vitals scores (TTI, TBT), increases cold-start parse and compile time in V8/SpiderMonkey, and imposes unnecessary bandwidth costs on end users, particularly on mobile and constrained connections.
EDIT
The drastic change in bundle size originally reported was not only related to MetaMask, but also changes to Rolldown in Vite 8.x. After reverting to 3.5.0 and rebuilding I'm now seeing the metamask bundle at around 540kB. To isolate more properly, I created minimal reproduction projects building with vite 8.0.4, and the difference is closer to 2x. All of the rest above is still valid, and 2x increase is unreasonable. The full list of dependencies:
vs