back

Rick · · Updated

Ethers vs Viem — the challenger gaining ground

Dev tooling

When I first wrote this article in July 2025, Ethers.js was clearly ahead and Viem was clearly climbing. Both of those things have changed enough to be worth remeasuring.

The headline number has flipped. In August 2026, Viem was downloaded 23.2 million times from npm against Ethers.js's 15.4 million. The first month Viem came out ahead was October 2025, and it has been ahead consistently since January 2026.

The bundle size story turned out to be more interesting than the download numbers. The comparison everyone repeats — roughly 31kB for Viem against 130kB or more for Ethers — is not what I measured. Depending on how you write Viem, the same task costs either about 30kB or about 100kB gzipped, and the 100kB version is the one most people actually ship.

This is a rewrite rather than a patch. The 2025 version leaned on a project-count snapshot I can no longer reproduce and on a summary of opinions from X, neither of which is evidence. What follows is what I could verify from primary sources on 8 September 2026, plus one measurement I ran myself.

What actually changed since July 2025

Ethers.js is on 6.17.0, published 18 June 2026. It is a substantive release: request rate throttling on providers, ENSv2 integration, forward and reverse ENS Universal Resolver improvements, and a transactionsRoot on blocks. This is not a project in maintenance mode.

But the cadence is very different from Viem's. Ethers shipped 11 stable versions in 2025 and one so far in 2026. Viem shipped 126 in 2025 and 74 so far in 2026, reaching 2.56.3 on 2 September 2026. In the six months to 8 September 2026, the Viem repository took 540 commits and the Ethers.js repository took 28.

Open issues tell a similar story from a different angle: 660 open on ethers-io/ethers.js against 12 on wevm/viem.

Both libraries are MIT licensed. That has not changed and is worth stating plainly, because it is the one dimension where there is genuinely no tradeoff.

Adoption: what is measurable and what is not

The original version of this article cited 101k projects using Ethers against 36k using Viem, from a scrape of Electric Capital and OpenQ data. I could not reproduce that snapshot, and I am not going to keep repeating a number I cannot check. Here are two measures that anyone can verify today.

Monthly npm downloads for ethers and viem from January 2023 to August 2026. Ethers grows steadily from about 4 million to 15.4 million per month. Viem starts near zero in early 2023, first passes ethers in October 2025, and reaches 23.2 million in August 2026.

npm downloads are the flow measure — what is being installed right now, including CI runs and registry mirrors. Viem passed Ethers there in late 2025 and the gap has widened every month since.

GitHub dependents are the stock measure — what is already built. On 8 September 2026, ethers-io/ethers.js reported 685,810 dependent repositories and 25,986 dependent packages. wevm/viem reported 101,615 repositories and 15,074 packages.

That split is the whole picture in two numbers. Ethers has 6.7 times as many dependent repositories, which is eight years of tutorials, bootcamp projects, and shipped applications. But among dependent packages — libraries that had to pick a dependency and could have picked either — the ratio is only 1.7 times. New code is choosing differently from old code.

Neither measure counts developers. Downloads inflate with CI frequency and transitive dependencies, and a large share of Viem's volume arrives through Wagmi, which peer-depends on viem@2.x. Treat both as directional.

One more caveat on stars, since they get quoted a lot: Ethers.js has 8,716 and Viem has 3,552. In this case stars are close to meaningless, because most people meet Viem through Wagmi, which has 6,745 of its own.

The bundle size claim, measured

This is the claim I most wanted to check, because the numbers in circulation are old and I had repeated them myself.

I wrote the same four tasks three times: once in Ethers.js, once in Viem the way most Viem code is written, and once in Viem the way Viem's own documentation recommends when you care about bundle size. Then I bundled each one and measured it.

The tasks are deliberately ordinary:

  1. Read an ETH balance and format it.
  2. Read an ERC-20 balance, decimals, and symbol, and format the result.
  3. Query historical Transfer logs and decode them.
  4. Sign and send an ERC-20 transfer from a private key.

The difference between the two Viem versions comes down to the imports and the call site. Here is the balance read in all three styles:

// src/ethers/balance.js — ethers 6.17.0
import { JsonRpcProvider, formatEther } from 'ethers'

export async function run({ rpcUrl, holder }) {
  const provider = new JsonRpcProvider(rpcUrl)
  const wei = await provider.getBalance(holder)
  return formatEther(wei)
}
// src/viem/balance.js — viem 2.56.3, default client with actions attached
import { createPublicClient, http, formatEther } from 'viem'

export async function run({ rpcUrl, holder }) {
  const client = createPublicClient({ transport: http(rpcUrl) })
  const wei = await client.getBalance({ address: holder })
  return formatEther(wei)
}
// src/viem-lean/balance.js — viem 2.56.3, bare client and a tree-shakable action
import { createClient, http, formatEther } from 'viem'
import { getBalance } from 'viem/actions'

export async function run({ rpcUrl, holder }) {
  const client = createClient({ transport: http(rpcUrl) })
  const wei = await getBalance(client, { address: holder })
  return formatEther(wei)
}

Same result, same JSON-RPC request. Under esbuild, the second version costs 102kB gzipped and the third costs 32kB. The only change is createPublicClient becoming createClient, and the action moving from a method on the client to an import.

All twelve programs were run against a local Anvil chain and the same deployed token. They all work, and they all return the same values. That matters: it would be easy to make a library look small by measuring code that does less.

The versions were Ethers 6.17.0, Viem 2.56.3, esbuild 0.25.10, Rollup 4.52.4 with terser, Node.js 22.17.0, and Anvil 1.5.1. Bundles are minified ESM for a browser target, then gzipped at level 9. The machine was an Apple M3 Pro. The generator, the three implementations, the harness, and the raw results are in the repository under examples/articles/ethers-vs-viem.

The results

Grouped bar chart of gzipped bundle size for four tasks, measured with esbuild and with Rollup. Ethers is 92 to 126 kB. Viem with a default client is 78 to 110 kB. Viem with tree-shakable actions is 19 to 65 kB.

All figures are kB gzipped, esbuild first and Rollup in parentheses.

Task Ethers Viem, default client Viem, tree-shakable actions
Read an ETH balance 94.5 (92.5) 102.0 (78.2) 32.3 (29.6)
Read an ERC-20 balance 94.6 (92.6) 102.1 (78.2) 33.8 (31.0)
Query Transfer logs 94.6 (92.6) 102.1 (78.2) 20.6 (18.8)
Send an ERC-20 transfer 125.9 (123.8) 109.9 (85.8) 52.4 (45.7)
All four in one bundle 126.3 (124.2) 110.2 (86.1) 64.6 (57.5)

Three things stand out.

Viem's small-bundle reputation is real, but conditional. The tree-shakable column is where the familiar ~31kB figure lives, and it holds up against what Viem claims for itself: 32.3kB for a balance read, and 2.4 to 4.6 times smaller than Ethers across the four tasks. The published claim is accurate. It just describes a style of code that is not the default.

The default Viem client is not small. createPublicClient decorates the client with every public action, so importing one action pulls in all of them. Reading a balance costs 102kB under esbuild — more than the equivalent Ethers program. Under Rollup it costs 78kB, less than Ethers but nowhere near 31kB.

The two bundlers disagree, and only about Viem. Ethers measures within 2kB either way. Viem's default client measures 24kB smaller under Rollup. Comparing the module graphs, the reason is deduplication rather than smarter tree-shaking: Rollup's resolver collapses the nested @noble/curves and @noble/hashes copies into one, and esbuild keeps both. So if you build with Vite, whose production build is Rollup, your numbers will look better than a developer on an esbuild-based pipeline. This is worth knowing before you quote any single figure, including mine.

Where the bytes come from

Breaking down the minified output for the balance read explains the shape of the table.

Ethers pulls in 179.8kB of its own code, 35.9kB of @adraffy/ens-normalize, and 36.9kB of @noble cryptography. The ENS normalization tables ship whether or not you resolve a name, because JsonRpcProvider includes the resolver.

Viem's default client pulls in 178.7kB of its own code plus 45.2kB of ox, 57.7kB of @noble/curves, and 20.1kB of @noble/hashes. The elliptic curve code is there twice, because npm placed a physical copy of @noble/curves under both Viem and ox rather than hoisting one. All of that is in a program that only reads a balance and signs nothing.

The tree-shakable version pulls in 86.2kB of Viem, 4.2kB of ox, and no elliptic curve code at all.

The signing task is where Ethers pays its own version of this tax: constructing a Wallet drags in 41.6kB of aes-js for JSON keystore encryption you may never use.

What this does not prove

This is one set of four tasks measured on one machine with one install tree.

  • A real application imports far more than four functions, and the fixed cost gets amortized across everything else you ship.
  • Package manager and lockfile choices change deduplication. The duplicate @noble/curves is an artifact of this npm tree, and it is exactly what Rollup's resolver collapses; pnpm or a resolution override would have a similar effect.
  • I measured esbuild and Rollup. Webpack, Turbopack, and Metro will produce different numbers.
  • Gzip level, Brotli, and HTTP compression settings all move the result. Brotli figures are in the raw JSON.
  • Bundle size is not load time. Parse and execute cost, caching, and code splitting matter and I did not measure any of them.
  • This says nothing about correctness, security, or API quality.

Where the APIs actually differ in practice

Writing the same four tasks three times surfaced differences that no bundle chart shows.

Number formatting is not identical. ethers.formatUnits renders a whole number as "999450.0". viem.formatUnits renders it as "999450". If you are migrating and comparing strings — in snapshot tests, for instance — this will bite you before anything interesting does.

Ethers caches provider calls by default. JsonRpcProvider holds results for 250ms, which is invisible until you send two transactions back to back and the second one reuses a stale nonce. Constructing the provider with cacheTimeout: -1 disables it. I hit this while writing the test fixture for this article.

Viem declares TypeScript 5.0.4 or later as an optional peer dependency, and ABI-driven type inference is the main reason people reach for it. Ethers declares engines.node >= 14 and no TypeScript floor. If you are on an older toolchain, that is a real constraint rather than a preference.

Viem is BigInt-only, and so is Ethers. The v6 migration guide is explicit that "the BigNumber class has been replaced with the built-in ES2020 BigInt." This stopped being a differentiator in 2023, though plenty of comparisons still list it as one.

I did not benchmark runtime performance here. Viem publishes its own encoding and parsing benchmarks; those are vendor results and I have not reproduced them. For what a rigorous head-to-head actually takes, see the Foundry and Hardhat benchmark where I did do that work.

Which one should you use

If this is true Choose Because
You are starting a new TypeScript frontend Viem, with viem/actions ABI-driven type inference through abitype, and the smallest bundles if you adopt the tree-shakable style from the start
You are using or planning to use Wagmi Viem Wagmi peer-depends on viem@2.x, and its docs recommend the same tree-shakable style
Bundle size is a hard budget Viem, with viem/actions 19 to 52kB gzipped per task against 93 to 126kB for Ethers
You use Viem but write client.getBalance(...) Viem, but change the style The default client costs 78 to 110kB, which gives up most of the advantage
You have a large existing Ethers codebase Ethers The download trend is not a reason to rewrite working code; Ethers 6 is still getting real releases
You are on Node-only backend code with no bundle Either Bundle size is irrelevant; pick the API your team knows
You specifically need ENSv2 Ethers 6.17.0 shipped ENSv2 integration; Viem's ENS actions document the Universal Resolver but not ENSv2
You are on TypeScript older than 5.0.4 Ethers Viem's peer requirement is a floor, and its types are the point of it
You depend on an Ethers-specific plugin Ethers Check the equivalent exists before committing to a migration

What I would say now

The 2025 version of this article said Ethers had not lost, but the trajectory mattered. That was right, and the trajectory has since arrived: Viem is now the more-installed library and the far more actively developed one, and it is the default for anyone entering through Wagmi.

What I got wrong was repeating the bundle size comparison without measuring it. Viem's advantage there is real and larger than I would have guessed — up to 4.6 times on a log query — but only in the tree-shakable style. Written the ordinary way, against a decorated client, Viem lands in the same range as Ethers and sometimes above it.

So the practical advice is narrower than "use Viem, it is smaller." It is: use Viem, and import your actions from viem/actions. If you are not going to do the second part, the bundle size argument is not yours to make.