There was a time when Lodash was pretty much the only game in town. That's not the case anymore. TypeScript adoption has crossed the point of no return, which means libraries without first-class type support are falling behind.
Bundle size now directly impacts Core Web Vitals, so you can't just throw in a heavy dependency without thinking about it. And ES2024+ has filled in enough gaps natively that libraries have to justify their existence more than they used to.
With that in mind, here are 10 libraries that are actually seeing real production use in 2026. The ordering is loosely by priority, but it depends on what you're building, so take it as a rough guide.
1. es-toolkit
npm install es-toolkit
Weekly downloads ~8.5M · GitHub ★ 10.9K
Built by the Toss team. Since launching in 2024, adoption has been growing fast, and it's already used in production by projects like Storybook, Recharts, ink, and CKEditor.
Three things matter here. First, bundle size is about 97% smaller than Lodash, roughly 2KB. Tree-shaking works properly, so only the functions you actually import end up in the bundle.
If your project is sensitive to CWV scores, that difference is significant. Second, it benchmarks 2-3x faster than Lodash. The implementations were written from scratch targeting modern JS engines rather than being backwards-compatible with IE-era runtimes. Third, TypeScript support is native.
Lodash relies on @types/lodash, which is maintained separately from the actual source, so the types occasionally drift from runtime behavior.
es-toolkit ships types from the same codebase as the implementation, so you don't get those mismatches. It also provides type guards like isNotNil out of the box.
// API is almost identical to Lodash
import { debounce, groupBy } from 'es-toolkit';
There's an es-toolkit/compat layer for incremental migration from Lodash. If you're on Vite, vite-plugin-es-toolkit can auto-rewrite your imports. The project maintains 100% test coverage, so reliability isn't a concern. For new projects, just use this instead of Lodash.
Site: https://es-toolkit.dev/
2. Lodash
npm install lodash
Weekly downloads ~72M (combined with lodash-es: ~92M) · GitHub ★ 61.5K
Still one of the most downloaded packages on npm. But that number is mostly legacy inertia. A lot of those downloads aren't direct usage but transitive dependencies from other packages that depend on Lodash.
If you're maintaining an existing codebase, or your team knows the Lodash API well enough that switching would create more churn than value, keep using it. Stability is proven, docs are thorough, and edge cases are well-handled.
That said, the reasons to pick it for a new project are shrinking. Tree-shaking is limited even with lodash-es, so your bundle stays heavy.
TypeScript types live in @types/lodash, managed by a different set of maintainers, so there are functions where the type definitions don't quite match the runtime behavior. Performance lags behind modern alternatives. It's going down the same road as jQuery: still everywhere, but not what you'd pick if you were starting fresh.
Site: https://lodash.com/
3. Zod
npm install zod
Weekly downloads ~102M · GitHub ★ 42K
TypeScript checks types at compile time only. Data coming in at runtime (API responses, user input, anything from the outside world) needs separate validation. Zod handles this. At 100M+ weekly downloads, the question of whether to use it is pretty much settled.
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(0).max(120),
});
type User = z.infer<typeof UserSchema>; // automatic type inference
const result = UserSchema.safeParse(apiResponse);
The key win is that a single schema gives you both the type definition and runtime validation. If you've been using Yup, it's worth comparing.
Yup added TypeScript support after the fact, so type inference is limited and methods like .cast() can produce types that don't match the actual output. Zod was designed in TypeScript from day one, so the type you get from z.infer matches the parse result exactly. Error message customization is more flexible too.
The ecosystem is broad. You can use it for tRPC API schemas, form validation with React Hook Form's zodResolver, or parsing environment variables from .env files. Zod 4 shipped in March 2026 with smaller bundle size and improved parse performance.
Site: https://zod.dev/
4. date-fns
npm install date-fns
Weekly downloads ~31M · GitHub ★ 36K
Moment.js went into maintenance mode in 2020, and date-fns is what filled the gap. The approach is functional: pure functions that never mutate the Date object, so you can do date math without worrying about side effects.
import { format, addDays, differenceInDays, isAfter } from 'date-fns';
import { enUS } from 'date-fns/locale';
const nextWeek = addDays(new Date(), 7);
format(nextWeek, 'MMMM d, yyyy (EEEE)', { locale: enUS });
// "April 2, 2026 (Thursday)"
Tree-shaking is the biggest practical win in production. Moment.js ships 72KB gzipped as a monolith. With date-fns, if you only use format + addDays, you're looking at about 6KB.
There are over 200 functions covering most date operations you'd need, with locale support for dozens of languages. TypeScript support is 100%, and the type definitions are hand-written rather than auto-generated, so they're more accurate.
Worth noting: the Temporal API hit Stage 4 at TC39 and is slated for the ECMAScript 2026 spec. Once browsers ship it, date libraries become less necessary. But actual browser support is still a ways out.
Site: https://date-fns.org/
5. Day.js
npm install dayjs
Weekly downloads ~30M · GitHub ★ 48K
2KB bundle size. That's the main reason to pick Day.js. The API is nearly identical to Moment.js, so migrating from Moment is mostly just changing the import. It also uses immutable objects, which fixes Moment's most infamous problem: mutating the original Date.
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
dayjs('2026-03-26').fromNow(); // "a few seconds ago"
dayjs().add(7, 'day').format('YYYY-MM-DD'); // "2026-04-02"
It's plugin-based, so you only load what you need. Features like relativeTime, timezone, and isBetween are separate plugins, keeping the core minimal. The chaining API is intuitive if you're coming from Moment, so the learning curve is basically zero.
The date-fns vs Day.js decision is straightforward. If you're TypeScript-heavy and prefer functional style, go with date-fns. If you're migrating from Moment, prefer a chaining API, or want the absolute smallest bundle, go with Day.js.
Site: https://day.js.org/
6. Radash
npm install radash
Weekly downloads ~1M · GitHub ★ 4.8K
Built by a former Google engineer, starting from the question: "What would Lodash look like if you designed it from scratch in 2023?" Downloads are lower than es-toolkit or Lodash, but the project has a different goal. It doesn't try to be API-compatible with Lodash. Instead, it proposes new patterns.
import { shake, try as tryit, retry, parallel } from 'radash';
// strip all falsy values from an object (like compact + pickBy combined)
shake({ a: 1, b: null, c: undefined, d: '' });
// { a: 1 }
// Go-style error handling for try-catch
const [err, result] = await tryit(fetchUser)(userId);
// auto-retry with backoff (3 attempts, 100ms delay)
const user = await retry({ times: 3, delay: 100 }, () => fetchUser(userId));
// parallel async with concurrency limit
const results = await parallel(3, userIds, async (id) => fetchUser(id));
tryit converts try-catch into Go-style [error, result] tuples, which is surprisingly nice once you get used to it. retry and parallel are async utilities that would be tedious to implement from scratch and you'd end up writing them yourself anyway. Overall it has that feeling of "someone already built the functions I was about to write."
Where es-toolkit focuses on being a Lodash drop-in replacement, Radash offers a different API entirely.
If you're migrating existing code, es-toolkit makes more sense. If you're starting a new project and want fresh patterns, check out Radash. The actively maintained fork Radashi is worth keeping an eye on too.
7. Nanoid
npm install nanoid
Weekly downloads ~40M · GitHub ★ 26.5K
ID generation in 118 bytes. The 40M weekly downloads partly come from other libraries using Nanoid internally (Redux Toolkit inlines it, for example), but standalone adoption is significant too.
Compared to UUID v4: UUID gives you 36 characters (9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d), Nanoid gives you 21 (V1StGXR8_Z5jdHi6B-myT). Nanoid is URL-safe out of the box since it only uses A-Za-z0-9_-, whereas UUID contains hyphens that can cause issues in certain URL contexts. Bundle size is 118B vs UUID's 423B. Security-wise they're equivalent because Nanoid uses crypto.getRandomValues().
import { nanoid } from 'nanoid';
nanoid(); // "V1StGXR8_Z5jdHi6B-myT"
// custom alphabets for things like order numbers
import { customAlphabet } from 'nanoid';
const orderid = customAlphabet('0123456789ABCDEF', 10);
orderid(); // "4F9A2B3C8D"
customAlphabet is useful in practice. Numeric-only IDs, hex-only, whatever your format requires. Unless your database or spec specifically demands UUID format, Nanoid is a better fit for most use cases.
8. Immer
npm install immer
Weekly downloads ~23M · GitHub ★ 28.9K
If you've worked with React or Redux, you know the spread operator nightmare when maintaining immutability on deeply nested objects. Three or four levels of nesting and the code becomes hard to read and easy to mess up. Immer fixes this.
import { produce } from 'immer';
// this is what it looks like without Immer
const newState = {
...state,
user: { ...state.user, address: { ...state.user.address, city: 'SF' } }
};
// and with Immer
const newState = produce(state, draft => {
draft.user.address.city = 'SF';
});
Under the hood it's copy-on-write. Immer tracks mutations to the draft proxy, creates new objects only for the parts that actually changed, and keeps original references for everything else. So you write code that looks like direct mutation, but the result is immutable. If nothing changes, it returns the original object by reference, which is good for re-render optimization.
Works well with Zustand. Wrap your store with zustand/middleware/immer and you can just state.users.push(user) directly. Redux Toolkit has Immer built in, so createSlice reducers use it automatically.
The reduced code volume in projects with complex nested state is nice, but the bigger win is that an entire class of bugs (forgetting to spread at some nesting level) goes away structurally.
9. Axios
npm install axios
Weekly downloads ~98M · GitHub ★ 108K
"Why not just use fetch?" is a fair question. For simple requests, yeah, fetch is fine. But in production, HTTP requests are usually not simple.
// automatically attach auth token to every request
axios.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
});
// auto-logout on 401
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) logout();
return Promise.reject(error);
}
);
Interceptors are the main reason Axios keeps getting picked. You can hook into the request/response cycle to handle auth tokens, error logging, and response transforms in one place. You can build a similar wrapper around fetch, but at that point you're reimplementing what Axios already does.
Beyond that, there are things fetch just doesn't do. Axios auto-parses JSON responses (fetch makes you call response.json() every time). Timeout is built in. Error handling is automatic based on HTTP status codes (fetch resolves on 404 and 500, which catches people off guard). Request cancellation works with AbortController in both, but Axios's CancelToken API is cleaner to use.
At ~98M weekly downloads and 108K GitHub stars, it's the de facto standard for HTTP clients in the JS ecosystem.
10. DOMPurify
npm install dompurify
Weekly downloads ~17M · GitHub ★ 16K
If you're rendering user input as HTML (markdown editors, WYSIWYG editors, comment systems, email previews), you need XSS protection. DOMPurify handles this.
import DOMPurify from 'dompurify';
const userInput = '<img src="x" onerror="alert(\'XSS\')">';
element.innerHTML = DOMPurify.sanitize(userInput);
// '<img src="x">' — onerror is stripped
It works by running HTML through the browser's native DOM parser, then removing anything that isn't on the allowlist. If you're tempted to filter HTML with regex instead, don't. The number of attack vectors is staggering: <img src=x onerror=...>, <svg onload=...>, javascript: URLs, mutation XSS, and many more. DOMPurify tracks and patches against these continuously.
Configuration is granular. You can whitelist specific tags with ALLOWED_TAGS, block specific attributes with FORBID_ATTR, or return plain text instead of HTML. For SSR environments where there's no DOM, use isomorphic-dompurify.
If user input ever touches innerHTML, use DOMPurify. This isn't a preference call. It's a security requirement.
How to choose
It depends on what you're building, but roughly:
General-purpose utilities: es-toolkit for new projects. es-toolkit/compat for Lodash migrations. Radash if you want a fresh API design.
Runtime type validation: Zod. Close to non-negotiable for TypeScript projects at this point.
Dates: date-fns if you want functional style and granular tree-shaking. Day.js if you're migrating from Moment or want the smallest possible bundle.
ID generation: Nanoid for most cases. uuid if your spec requires the UUID format.
Immutable state: Immer, especially if you have deep nesting.
HTTP client: Axios if you need interceptors, retries, and timeouts. Fetch if your needs are simple.
HTML sanitization: DOMPurify whenever user input gets rendered as HTML.
Site: https://cure53.de/purify
The broader trend is clear. TypeScript support is table stakes. Bundle size matters. Libraries that don't tree-shake are getting left behind. ES2024+ native features are reducing the surface area that libraries need to cover in the first place.
You don't need all 10 of these tomorrow. But knowing what each one does and when it's the right tool saves you time when the need comes up.
Top comments (0)