Both let you write modern JavaScript for older engines, but they fix different problems.
- Polyfill: JavaScript that runs in the user's engine and defines a missing API (
Array.prototype.at,Promise.withResolvers,structuredClone) when it is absent. It works because adding a property needs no new syntax. Examples:core-js, or a hand-written, feature-detected shim. - Transpiler: a build-time tool that compiles source to source, rewriting new syntax (
??,?., classes,asyncfunctions) into older equivalents. It has to run before the code ships, because an engine that does not know the syntax throws aSyntaxErrorfor the whole file. Examples: Babel, SWC, and esbuild (which also bundles and minifies).
The rule of thumb: new syntax is transpiled, new built-ins are polyfilled. Some features need both: a transpiled async function or generator relies on a runtime helper, and on Promise, which may itself need a polyfill.
Neither is perfect. A transpiler can only emit what the older language can express, so some features are approximated with heavier output (esbuild lowers private #fields to WeakMap lookups). A polyfill can only use what exists, so behaviour that needs engine support cannot be reproduced faithfully.
Both are driven by target environments. A Browserslist query, or a tool option such as esbuild's target, states which engines you support, and the tool applies only the transforms and polyfills those engines lack. A wider target means larger, slower output.