Safeguard
DevSecOps

vite-plugin-node-polyfills: A Security-Minded Setup Guide

vite-plugin-node-polyfills injects browser shims for Node built-ins so npm packages that expect Buffer or process work in Vite. Convenient, but every polyfill you add is code that ships.

Marcus Chen
DevSecOps Engineer
5 min read

vite-plugin-node-polyfills is a Vite plugin that provides browser-compatible shims for Node.js built-in modules like Buffer, process, crypto, and stream, so that npm packages written for Node can run in a browser bundle. It is the modern replacement for the manual Rollup polyfill juggling that Vite users used to do, and it works well. The security angle is simple: everything it polyfills becomes real JavaScript that ships to the browser, so the goal is to enable the smallest set of shims your dependencies actually need.

Vite deliberately does not polyfill Node built-ins by default, because Vite targets the browser and most web code should not depend on Node internals. When a dependency does (a crypto library reaching for crypto, a package assuming Buffer exists), you hit a build or runtime error, and this plugin is the common fix.

What the plugin does

Install and register it, and it wires up shims for the Node standard library through Vite's resolver:

import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';

export default defineConfig({
  plugins: [
    nodePolyfills({
      include: ['buffer', 'process'],
      globals: { Buffer: true, process: true },
    }),
  ],
});

Behind the scenes it draws on the same shim implementations that the older rollup-plugin-node-polyfills provided, packaged for Vite's plugin API. If you have used the Rollup plugin before, the concept is identical: swap Node's real modules for browser-safe reimplementations at bundle time. The difference is that vite node polyfills integrate with Vite's dev server and production build in one plugin, so you configure it once rather than maintaining separate dev and build paths.

Why the include list matters

The tempting move is to call nodePolyfills() with no options and let it shim everything. Resist it. Two reasons.

First is bundle size. A crypto or stream polyfill is not small, and shipping the whole Node standard library to every visitor when your code only touches Buffer is wasted bytes. Scoping the include array to the exact modules you need keeps the payload lean.

Second is attack surface. Every polyfilled module is code executing in your users' browsers. A vulnerability in a shim implementation, or in one of its transitive dependencies, becomes a vulnerability in your app. The narrower your include list, the less third-party code you are responsible for. This is the same principle as minimizing production dependencies generally, applied to the browser bundle.

Figure out what you actually need by letting the build tell you. Start with an empty polyfill set, build, and read the "module externalized for browser compatibility" warnings Vite emits. Those name the exact Node modules your dependencies reach for. Polyfill only those.

The Buffer and process globals

Two shims deserve special mention because packages assume they exist as globals, not imports. Buffer and process were ambient in Node, so a package might write Buffer.from(...) without importing anything. The globals option injects these as true globals so that code works. Only enable the globals you need; injecting process as a global also pulls in process.env, and you do not want to accidentally leak build-time environment variables into a client bundle.

That last point is a real risk. If a dependency reads process.env.SOME_KEY and you have polyfilled process with your environment inlined, a secret can end up in shipped JavaScript. Keep client-side environment variables to the VITE_ prefixed set Vite exposes deliberately, and never rely on a process shim to carry configuration.

Relationship to rollup-plugin-node-polyfills

Because Vite builds on Rollup for production, the lineage is direct. rollup-plugin-node-polyfills predates the Vite ecosystem and is still used in pure Rollup projects. For Vite specifically, use vite-plugin-node-polyfills instead, because it also handles the dev server's esbuild-based dependency pre-bundling, which the Rollup-only plugin does not. Mixing the two, or using the Rollup plugin alone in a Vite project, tends to produce the "works in build, breaks in dev" class of bug.

Whichever you use, both are build tooling, so pin their versions and include them in your dependency audit. A vite plugin node integration runs with your build's privileges, so a compromised release is a build-time supply-chain risk, not just a bundle-size one. An SCA scan across your lockfile catches advisories in these plugins and their transitive shims before they reach a release.

A lean, safe configuration

Putting it together, a defensible setup looks like:

  • Enable polyfills only for modules the build warnings prove you need.
  • Enable Buffer/process globals only if a dependency uses them ambiently.
  • Never route configuration or secrets through a polyfilled process.
  • Pin the plugin version, commit the lockfile, and scan dev dependencies.
  • Re-audit the include list whenever you add or upgrade a dependency, since new packages may add new Node requirements.

Treated this way, node polyfills are a pragmatic compatibility layer rather than a quiet source of bloat and exposure.

FAQ

Why does Vite not polyfill Node built-ins by default?

Vite targets the browser, where Node's standard library does not exist and most web code should not depend on it. Polyfilling is opt-in so you only ship shims when a dependency genuinely needs them.

Should I use vite-plugin-node-polyfills or rollup-plugin-node-polyfills?

For a Vite project, use vite-plugin-node-polyfills. It handles both the esbuild-based dev server and the Rollup production build. The Rollup-only plugin misses the dev path and tends to cause dev-vs-build discrepancies.

Can polyfilling process leak secrets into my bundle?

Yes, if a dependency reads process.env and your process shim carries inlined environment values. Keep client variables to Vite's VITE_ prefixed set and never depend on a process polyfill for configuration.

How do I know which modules to include?

Build with no polyfills first and read Vite's "externalized for browser compatibility" warnings. They name the exact Node modules your dependencies use. Polyfill only those to keep bundle size and attack surface small.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.