Plate 58
Pathe vs node:path: 1.1 KB for Cross-OS Paths
Aditya Challa5 min read
I bundled pathe 2.0.3 for the browser at 2,268 B minified / 1,091 B gzip. node:path will not resolve in a browser esbuild build at all. On the same box, a mixed join/normalize/resolve workload ran about 38,000 ops/s with pathe versus about 114,000 ops/s with node:path — roughly 3× slower for the portable option.
Short answer: keep node:path in Node-only code. Add pathe when the same path helpers must run in the browser, in isomorphic Vite plugins, or when you need Windows backslashes normalized to / on every OS. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 01:48 IST.
- Versions: pathe 2.0.3, Node 22.20.0 built-in node:path and node:path/posix, esbuild 0.28.2 minify + gzip -9.
- Fixture: /workspace/bench-pathe-node-path/. Workload: 8 sample path tuples run through join, normalize, basename, dirname, extname and resolve each iteration, 20,000 iterations per round, 9 rounds, median, after warm-up. Two runs with the order reversed. Bundle attempt: platform browser for all three; node:path and node:path/posix fail to resolve unless platform is node.
- Findings: pathe 37,231 and 38,561 ops/s; node:path 113,894 and 114,507; node:path/posix 114,895 and 113,381. pathe gzip 1,091 B. On Linux, pathe treated C:\a\b as absolute and rewrote mixed slashes to /; node:path did not.
- One surprise: node:path/posix was not a free "browser path" stand-in. It still needs the Node built-in, so esbuild browser builds reject it the same way as node:path.
- Not tested: Windows host timings, Bun/Deno path APIs, URL fileURLtoPath, very long UNC paths, and tree-shaking of individual pathe named exports beyond the join/normalize/resolve set I imported.
How much does pathe cost in a browser build?
| Build | Minified | gzip -9 | Browser esbuild |
|---|---|---|---|
| pathe 2.0.3 (named exports I used) | 2,268 B | 1,091 B | ok |
| node:path (platform browser) | — | — | resolve error |
| node:path (platform node, external) | 266 B stub | 162 B | N/A (Node only) |
The stub row is esbuild leaving the built-in external under platform node; it is not a browser polyfill. For front-end budgets, size-limit vs bundlesize will see about 1.1 KB. Vite-era tooling often already depends on pathe transitively — check with knip vs depcheck before adding another copy.
Is pathe slower than node:path?
| Workload (8 samples × join/normalize/basename/dirname/extname/resolve, median of 9) | pathe | node:path | node:path/posix |
|---|---|---|---|
| run 1 (pathe first) | 37,231 ops/s | 113,894 ops/s | 114,895 ops/s |
| run 2 (order reversed) | 38,561 ops/s | 114,507 ops/s | 113,381 ops/s |
Yes, about 3× on this Linux box. That gap rarely matters for a few path joins per request or per plugin hook. It would matter in a hot loop over tens of thousands of file paths — the kind of scan tsx vs ts-node or a bundler entry walk might do on the server, where node:path stays free and faster.
What does pathe change on Linux?
| Input | pathe | node:path (Linux) |
|---|---|---|
| join("C:\Users\a", "src\main.ts") | C:/Users/a/src/main.ts | C:\Users\a/src\main.ts |
| normalize("foo\bar/baz/../qux") | foo/bar/qux | foo\bar/qux |
| isAbsolute("C:\a\b") | true | false |
| isAbsolute("/a/b") | true | true |
| sep | / | / |
| relative("/a/b/c", "/a/b/d") | ../d | ../d |
pathe is a POSIX-slash path helper that still understands Windows prefixes. That is why Vite plugins and unjs tools pick it for code that may run under browser esbuild or on a mixed CI matrix. Pure Node services on Linux can stay on node:path and avoid the 1.1 KB. If your toolchain question is broader, see Vite vs Parcel and esbuild-register vs jiti.
Which one should you pick?
| Situation | My pick |
|---|---|
| Node-only scripts and servers on one OS | node:path |
| Shared Vite plugin / isomorphic utils | pathe |
| Browser bundle that must join paths | pathe |
| Hot loop over huge file lists on the server | node:path |
| Need Windows paths correct while developing on Linux | pathe |
Bottom line: pathe costs about 1.1 KB gzip and ran about 3× slower than node:path here, in exchange for browser builds and consistent / slashes across OS strings. Who should not add it: a pure Node CLI that never leaves the server and never sees backslash inputs. Package-manager and runtime choices around that CLI are covered in pnpm vs npm vs bun.
How this was made: I installed pathe on the ShopperCove box, measured esbuild minify+gzip for browser and node platforms, timed the mixed path workload in two order-reversed runs against node:path and node:path/posix, compared slash and isAbsolute behavior on Windows-style strings, and saved the JSON results. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/unjs/pathe
- https://www.npmjs.com/package/pathe
- https://nodejs.org/api/path.html
- https://nodejs.org/api/path.html#pathposix
Related
- https://www.shoppercove.com/blog/vite-vs-parcel
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/pnpm-vs-npm-vs-bun
- https://www.shoppercove.com/blog/esbuild-register-vs-jiti
- https://www.shoppercove.com/blog/tsx-vs-ts-node
- https://www.shoppercove.com/blog/tinyexec-vs-execa
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/unocss-vs-tailwind
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~01:48 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). pathe 2.0.3 vs built-in node:path and node:path/posix, esbuild 0.28.2 minify + gzip -9. Fixture /workspace/bench-pathe-node-path/. Size: pathe 2,268/1,091 B browser-ok; node:path browser build errors with unresolved built-in; platform node stub 266/162 B. Speed (8 sample tuples × join/normalize/basename/dirname/extname/resolve, 20k iters, 9-round median, order-reversed): pathe 37,231 / 38,561 ops/s; node:path 113,894 / 114,507; posix 114,895 / 113,381. Behavior (res-behavior.json): pathe join/normalize turn Windows separators into / and isAbsolute("C:\a\b") is true; node:path on Linux keeps backslashes and returns false. Raw: res-bundle.json, res-speed-run1.json, res-speed-run2.json, res-behavior.json. Not tested: Windows host, Bun/Deno, UNC, per-export tree-shake beyond the imported set. No affiliate.