blockchainsv
Web3 Integration & APIs·August 01, 2026·14 min read

Ethers.js vs Viem: performance and developer experience trade-offs

A frontend can feel slow long before a transaction reaches the mempool. The familiar version of this problem is a dashboard that renders twelve token balances, five vault positions, allowance state…

Ethers.js vs Viem: performance and developer experience trade-offs

A frontend can feel slow long before a transaction reaches the mempool. The familiar version of this problem is a dashboard that renders twelve token balances, five vault positions, allowance state, and a pending rewards number—then fires a small storm of RPC calls on every account or chain change. The RPC provider starts returning 429s, React state arrives out of order, and the user sees 0.00 for half a second before the real portfolio appears.

That is where an ethers.js vs viem performance comparison becomes useful—but only if we resist the neat but misleading verdict that one library is “faster.” There is no universal benchmark that can prove that across every chain, RPC tier, bundler, operation mix, cache state, and wallet. What we can compare is the architecture each library gives us, where requests can be consolidated, how much code our build can discard, and how much integration work our team carries into production.

Ethers v6 and viem are both capable Ethereum libraries. Both use native JavaScript bigint. Both can talk to HTTP, WebSocket, and EIP-1193 wallet providers. The trade-off is less about raw capability than about where each library places the seams.

Stateless clients versus a contract-centric API

Ethers has earned its place in many production codebases because its mental model is close to how teams describe an onchain application: there is a provider, there is a signer, there is a contract, and the contract exposes methods. For contract-heavy products, that remains a very pleasant path.

A typical ethers v6 flow looks conceptually like this: create a JsonRpcProvider, instantiate a Contract with an ABI and runner, then read, write, query logs, or subscribe to events through that contract object. Its Contract interface is powered by an ES6 Proxy, resolving ABI method names dynamically at runtime. It also accepts human-readable ABI fragments, which can keep small integrations tidy.

Viem asks us to be more explicit about the capability we need. It separates access into Public Clients, Wallet Clients, and Test Clients:

  • A Public Client reads chain data, simulates calls, estimates gas, and accesses public RPC methods.
  • A Wallet Client represents an account-capable connection for signing and wallet-mediated requests.
  • A Test Client targets local-node and testing workflows.
  • The transport is configured independently: HTTP, WebSocket, or a custom EIP-1193 transport.

That separation can initially feel like more ceremony, especially for a team that has used new Contract(...) for years. But in a frontend with multiple sources of state—public RPC, injected wallet, indexer, cached server data—the explicit split often removes ambiguity. We can see which reads are public, which need wallet context, and which parts of the user journey must react to account or chain changes.

ConcernEthers v6Viem
Main abstractionContract-oriented API around provider/signer/contractStateless actions attached to explicit clients
Contract interactionRuntime-resolved contract methods via ProxyABI-driven functions such as reads, writes, simulations, and event queries
Wallet browser integrationBrowserProvider wraps an EIP-1193 providerCustom transport can wrap an EIP-1193 request function
Best initial fitExisting ethers codebases and contract-centric workflowsTyped frontend systems with clear public/wallet boundaries
State-management implicationOften application-managed around provider and contract objectsNaturally composes with client-based and hook-based architectures

Neither model makes React state synchronization disappear. We still need to invalidate data after a confirmed write, handle a user rejecting a signature, and prevent stale reads from overwriting new chain state. But viem’s lower-level, stateless style makes those boundaries hard to ignore—which is usually a gift after the third production incident involving a stale chainId.

Performance is not a property of a package in isolation. It is a property of a user journey, an RPC policy, a cache, and the number of accidental requests we allow through.

Bundle size is an import discipline, not a slogan

The viem bundle size vs ethers discussion is often reduced to one sentence: “viem is more tree-shakable.” There is truth in the direction, but not enough in that sentence to make a release decision.

Viem is designed around composable, tree-shakable modules. Its API surface is broken into focused actions and utilities, so an application that imports a narrow set of functions gives a modern bundler a reasonable chance to omit the rest. That design is particularly appealing for browser products where the wallet connection, token metadata, transaction simulation, and contract reads already compete for the same initial JavaScript budget.

Ethers v6 also moved toward a more modular package structure than older versions, and teams should not assume a fixed bundle penalty without measuring their own build. The final output depends on much more than the dependency name:

  • the exact imports we use;
  • whether imports are package-level or narrow subpath imports;
  • the bundler and its tree-shaking behavior;
  • production minification settings;
  • target browsers and any required polyfills;
  • duplicate transitive dependencies;
  • whether the application ships large JSON ABIs, icons, analytics SDKs, or wallet connectors alongside the library.

A DApp’s ABI assets can be more consequential than the choice between two libraries. We have seen teams debate a few kilobytes of client code while shipping several sprawling protocol ABIs directly into the first route. That is not a critique of ABIs—we need them—but it is a reminder to inspect the build artifact before declaring victory.

For a useful frontend Ethereum library benchmark, pin the library versions, lock the same bundler configuration, create comparable entry points, and inspect the production output. At the time of the reviewed package listings, viem was indexed as version 2.55.8 and ethers as 6.17.0. Those numbers are not a permanent performance fact; they are part of the benchmark record. Change the version, and the conclusion may change with it.

A practical measurement sequence is:

1. Build a realistic route, not an empty import test. Include the wallet connection, one representative contract read, one transaction flow, and the ABI fragments the route genuinely needs.

2. Produce a production build with source maps or a bundle analyzer enabled.

3. Compare parsed and compressed sizes, because users download compressed assets but parse JavaScript after it arrives.

4. Repeat after removing dead ABI fragments and lazy-loading screens that do not belong in the first render.

5. Treat the result as a property of that application and build pipeline—not a league table for all of Web3.

This is not glamorous work. It does, however, catch the friction users actually feel.

Multicall aggregation and JSON-RPC batching solve different problems

This is the most important technical distinction in the performance conversation.

Viem can aggregate eligible concurrent eth_call reads through Multicall3 when batch.multicall is enabled on a client. Instead of asking an RPC endpoint to process many separate call requests, viem can package reads into an onchain Multicall3 aggregate3 request. The feature is disabled by default. When enabled, its documented default wait is 0 ms, meaning it gathers calls made in the current JavaScript message queue rather than deliberately delaying the UI. The default calldata chunk limit is 1,024 bytes.

That can make a meaningful difference for a portfolio or trading screen that triggers several independent read calls together: token balances, allowances, pool state, fee tiers, reward counters, and so on. The point is not magic speed. The point is fewer remote round trips and a more coordinated read path.

Viem’s explicit multicall action also uses Multicall3 and, by default, allows individual calls to fail without collapsing the entire result. That failure isolation is valuable in interfaces where one exotic token contract should not blank the user’s entire account view. We can render the healthy data, mark the failed section, and retry with context instead of presenting a generic loading failure.

Ethers v6 has a different optimization at the provider level. JsonRpcApiProvider can batch JSON-RPC requests, with controls including batchStallTime, batchMaxSize, and batchMaxCount. Its documented defaults include a 10 ms batching window and a 1 MB target maximum batch size. In this model, multiple JSON-RPC messages may travel together to the endpoint.

The two approaches can both reduce overhead, but they are not interchangeable:

DimensionViem Multicall aggregationEthers v6 JSON-RPC batching
Unit being combinedEligible contract reads based on eth_callJSON-RPC requests sent to a provider
Execution pathMulticall3 contract aggregates calls onchainRPC endpoint receives a batch of requests
Default statusDisabled unless batch.multicall is enabledConfigurable provider behavior with a documented 10 ms stall default
Failure behaviorMulticall results can preserve per-call success/failureDepends on individual RPC responses and application handling
Main frontend benefitConsolidates related contract readsReduces request transport overhead for concurrent RPC calls

That distinction matters when we diagnose a sluggish screen. If the slow part is remote RPC latency from a dozen eth_calls, Multicall can change the request shape. If the app emits mixed JSON-RPC methods concurrently, provider request batching may help reduce transport chatter. If the bottleneck is an overloaded indexer, a 900 ms cache miss, a waterfall caused by React effects, or a rate-limited endpoint, neither feature will rescue the experience by itself.

Let’s also be honest about the 0 ms setting. It does not mean zero latency. It means viem does not add an intentional waiting period beyond the current JavaScript queue before grouping eligible calls. The actual user-perceived time still includes encoding, network travel, RPC handling, node execution, decoding, React rendering, and whatever state synchronization follows.

Type safety changes the cost of frontend mistakes

The viem vs ethers.js syntax debate usually begins as a matter of taste. Ethers reads like a compact contract interface; viem reads more like a set of strongly specified operations. Over time, this becomes less about taste and more about where mistakes are caught.

Viem uses ABI-driven TypeScript inference as a core design goal. With a const ABI, function names, argument shapes, and return types can flow into the call site. That is especially helpful in a typed frontend where contract interfaces change frequently and several developers touch the same integration layer.

Consider the ordinary ways Web3 UI breaks:

  • a contract method is renamed while a stale frontend call remains;
  • arguments are passed in the wrong order;
  • a read function is accidentally routed through a wallet-dependent path;
  • a returned integer is formatted as if it were a decimal token amount;
  • a chain switch occurs between preparing a transaction and asking the user to sign it.

Strong ABI inference will not solve the last two on its own. It does shrink the set of silent API-shape mistakes that reach runtime. For a team building quickly across multiple contracts, that reduction in friction can outweigh any small differences in local execution time.

Ethers v6 is not untyped. It modernized substantially, including its move from the v5 BigNumber class to native ES2020 bigint. This point is worth stating because older migration advice still circulates: neither ethers v6 nor viem requires a separate BigNumber model for standard integer values.

Where ethers can feel more flexible is with human-readable ABI fragments and its familiar contract-oriented surface. A team maintaining an established ethers codebase may move faster by preserving that mental model, strengthening its TypeScript wrappers, and improving request orchestration around it. Rewriting a healthy integration because another API looks cleaner on social media is rarely the best use of a sprint.

The stronger question is: where do we want our integration rules to live?

With viem, many teams make a dedicated module for chain configuration, typed ABIs, public client actions, and wallet actions. With ethers, many teams centralize provider construction and contract factories. Both can be production-grade. Both can also become tangled if every React component creates its own client, fetches in an effect, and invents its own loading semantics.

React, wagmi, and the cost of integration glue

For React applications, ecosystem alignment is often the decisive factor. Wagmi is built on viem. That means current wagmi hooks normally use viem for underlying blockchain operations, and the pieces line up around a shared model of chains, transports, accounts, and query behavior.

If we are building a new React DApp with wagmi, using viem directly for custom reads or lower-level actions tends to reduce integration glue. Types travel more consistently, client configuration is less duplicated, and we do not have to translate between two competing abstractions in every edge case.

That does not make ethers a bad choice in React. It means an ethers-based application must be intentional about its architecture. We may manage providers, contract instances, effects, query caching, and wallet events ourselves, or introduce a separate integration layer. Plenty of teams do this well, especially when they have mature utility code and domain-specific wrappers.

The wallet boundary deserves particular care. Ethers v6 renamed Web3Provider to BrowserProvider, which wraps EIP-1193 providers such as injected browser wallets. Viem can use a custom transport backed by an EIP-1193 request function. In either setup, a wallet is not a static RPC endpoint. Accounts can change. Chains can change. Permissions can disappear. A user can reject a request at the final step.

We should design for graceful degradation:

  • Keep read-only public RPC access available when a wallet is disconnected.
  • Subscribe to account and chain changes, then invalidate or refetch the state derived from them.
  • Do not present a quote, allowance, or gas estimate as permanent truth after the network changes.
  • Separate “wallet connected” from “transaction ready.” A connected wallet on the wrong chain is a recoverable state, not a fatal error.
  • Surface partial read failures without erasing the data that did load.

And one sharp warning from the ethers documentation: do not use staticNetwork with a wallet-controlled provider such as MetaMask merely to skip a chain-ID request. If the wallet network changes, treating the network as fixed can have serious consequences. The small optimization is not worth corrupting the user journey.

The library choice matters most where it reduces our own architectural confusion. Users do not care which package won; they care that their balance, chain, and transaction state agree.

How to run a comparison your team can trust

If the decision is still open, we should benchmark the workflow we intend to ship rather than a library logo. A credible comparison records the details that actually move the result:

1. Exact versions. Pin ethers, viem, wagmi if present, and the relevant wallet connector versions.

2. Runtime and build environment. Browser version, Node version, bundler, minifier, target settings, and whether development tooling is excluded.

3. Network and RPC conditions. Chain, endpoint vendor, geographic region, authentication tier, rate limit, and cache state.

4. Operation mix. Separate a single balance read, ten parallel reads, log queries, transaction preparation, wallet signing, and event subscriptions. These are different workloads.

5. Batch configuration. State whether viem Multicall is enabled and what its chunk settings are; state ethers provider batch settings as well.

6. User-facing metrics. Record initial route JavaScript, time to useful data, number of RPC requests, failed-request rate, and stale-state incidents during account or chain switches.

The final item is the one teams often skip because it is harder to turn into a chart. Yet a library that saves a small amount of bundled JavaScript while producing fragile account state is not helping the product. Conversely, a heavier abstraction may be entirely justified if it lets the team ship correct transaction flows, monitor failures, and support the protocol without fear.

Choose the seam that keeps your frontend coherent

For a new React application already leaning on wagmi, viem is often the path of least resistance. Its client model, ABI-based inference, and optional Multicall aggregation fit naturally with a typed, query-driven frontend. Enable Multicall deliberately, test it against the contracts and RPC infrastructure you use, and handle partial failures as part of the UI design.

For an established ethers application, ethers v6 remains a capable and modern foundation. Its native bigint support, BrowserProvider, contract interface, event tooling, and configurable JSON-RPC batching are not legacy compromises. If the existing integration is clear, measured, and well-tested, migration needs a stronger reason than a generic claim about speed.

The useful answer to “ethers or viem?” is therefore a little less dramatic: choose the library whose request model, type boundaries, and ecosystem fit reduce friction in the code your users depend on every day. Then measure the route, watch the RPC behavior, and fix the state sync failures that no package can fix for us.

FAQ

What is the main architectural difference between Ethers v6 and Viem?
Ethers v6 relies on a contract-oriented API using dynamic runtime Proxy objects built around Providers, Signers, and Contracts. Viem uses a stateless architecture with explicit Public, Wallet, and Test Clients paired with composable, ABI-driven helper functions.
How do Viem and Ethers handle batching multiple blockchain reads?
Viem can aggregate concurrent contract call reads onchain using Multicall3 when `batch.multicall` is enabled. Ethers v6 handles request batching at the provider level using `JsonRpcApiProvider`, which groups multiple JSON-RPC calls into batched HTTP messages sent to the RPC endpoint.
Is Viem faster and smaller in bundle size than Ethers v6?
Viem is designed with composable modules that make it highly tree-shakable, but actual bundle size and performance depend on build configurations, tree-shaking rules, specific subpath imports, and the size of imported protocol ABIs. Ethers v6 also features a modernized, modular structure.
Which library is better for React applications using Wagmi?
Viem is the default choice for modern React applications using Wagmi, as Wagmi is natively built on top of Viem. Using Viem directly minimizes integration code and ensures shared client and type definitions across the application.
Do Ethers v6 or Viem still require external BigNumber libraries?
No, neither library requires a separate BigNumber package for standard integer handling. Both Ethers v6 and Viem utilize native JavaScript `bigint` for managing Ethereum integers.

By Chloe Redfern