Moment.js在前端圈火了将近十年,几乎是日期处理的代名词。但它的维护团队已经在官方文档中明确宣布进入维护模式,不再开发新功能,并建议新项目不要再用它。对于React项目来说,这确实是个需要认真对待的问题:一个6KB起步(gzip后约70KB的功能集)的库,加上可变的对象设计和不支持Tree Shaking的模块结构,在现代打包工具下显得越来越不合时宜。替代方案里呼声最高的就是Day.js和date-fns,这篇文章就来聊聊这两个库各自的特点,以及怎么从Moment.js平滑迁移过去。

为什么Moment.js逐渐被弃用
首先要理解Moment.js被抛弃的原因,才能明白替代方案解决了什么问题。第一点是体积。Moment.js的完整包gzip后大约有70KB,在追求首屏加载速度的今天,这个体积对于“只是格式化一个日期”的需求来说太重了。虽然可以通过locale定制减小体积,但操作繁琐,效果有限。
第二点是不可变性。Moment的对象是可变的,调用moment().add(1, 'day')会直接修改原对象。这在React里尤其危险,因为React依赖引用比较来判断是否需要重渲染。如果你把moment对象存在state里,然后调用方法修改它,引用没变,组件不会重渲染,界面就会出现数据变了但视图不更新的诡异bug。
第三点是模块化设计。Moment.js是整体加载的,即使你只用了一个format方法,打包时也会把整个库都打进去,Tree Shaking对它基本无效。官方文档对此的解释很坦率:这是历史遗留的架构问题,无法在现有版本上修复。
Day.js:迁移成本最低的选择
如果你的项目里已经有大量Moment.js代码,Day.js几乎是为你量身定做的。它的API设计刻意保持了与Moment.js的高度兼容,大部分代码只需要把moment换成dayjs就能继续跑。先看安装:
npm install dayjs
基本用法对比一下就很直观。原来的Moment.js写法:
import moment from 'moment';
const now = moment().format('YYYY-MM-DD HH:mm:ss');
const nextWeek = moment().add(7, 'day').format('YYYY-MM-DD');
const fromNow = moment('2020-01-01').fromNow();换成Day.js之后:
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
// 插件需要手动注册,这是与Moment最大的差异
dayjs.extend(relativeTime);
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');
const nextWeek = dayjs().add(7, 'day').format('YYYY-MM-DD');
const fromNow = dayjs('2020-01-01').fromNow(); // 几年前注意插件机制这一差异。Moment.js把所有功能都内置了,而Day.js把相对时间、时区、日历、季度等功能拆成了插件,用哪个引入哪个。这个设计正是它能把体积控制在2KB的核心原因。常用的插件有这些:
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isSameOrBefore);
// 时区转换示例
const time = dayjs('2024-06-01 12:00').tz('Asia/Shanghai').format();
const nyTime = dayjs(time).tz('America/New_York').format('YYYY-MM-DD HH:mm');中文语言包也是按需加载的,只会在最终产物里增加约2KB,而不是Moment那样动辄几十KB:
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
dayjs().format('dddd'); // 星期六另外重要的一点是,Day.js的对象是不可变的。dayjs().add(1, 'day')返回新实例,不会修改原对象,这正好契合React的数据流理念,存在state或props里都很安全。
date-fns:函数式风格与Hooks的天然搭配
如果你更看重Tree Shaking的极致效果,或者喜欢函数式的代码风格,date-fns值得认真考虑。它和Moment的API完全不同:没有日期对象的概念,所有函数都接收Date对象或时间戳作为参数,返回新值。
npm install date-fns
基本用法是这样的:
import { format, addDays, formatDistanceToNow, differenceInDays } from 'date-fns';
import { zhCN } from 'date-fns/locale';
const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
const nextWeek = format(addDays(new Date(), 7), 'yyyy-MM-dd');
const diff = differenceInDays(new Date(), new Date('2024-01-01'));
const fromNow = formatDistanceToNow(new Date('2024-01-01'), { locale: zhCN });注意格式占位符的区别:Moment和Day.js用大写的YYYY-MM-DD,date-fns v2以后用小写的yyyy-MM-dd,迁移时这是最容易踩的坑,大小写写错不会报错,只是输出结果不对,比如大写MM在小写位置的语义完全不同。
date-fns在React Hooks场景下用起来非常顺手。配合useMemo可以精确控制计算时机:
import { useMemo } from 'react';
import { formatDistanceToNowStrict } from 'date-fns';
import { zhCN } from 'date-fns/locale';
function TimeAgo({ timestamp }) {
const text = useMemo(
() => formatDistanceToNowStrict(new Date(timestamp), { locale: zhCN, addSuffix: true }),
[timestamp]
);
return <span>{text}</span>;
}Tree Shaking在这里发挥到极致:打包工具只会把你import进来的函数打进产物,单个函数平均只有几百字节。如果你的项目只在两三个地方用到日期格式化,date-fns可能是体积最优解。
三者对比与选型建议
把关键维度整理成表格方便对比:
| 维度 | Moment.js | Day.js | date-fns |
|---|---|---|---|
| gzip体积 | 约70KB | 约2KB | 按需,通常几KB |
| API风格 | 链式对象 | 链式对象 | 纯函数 |
| 不可变性 | 可变对象 | 不可变 | 不可变 |
| Tree Shaking | 不支持 | 支持(插件级) | 完美支持 |
| 时区支持 | 内置 | 插件 | v2集成Temporal或第三方 |
| 迁移成本 | — | 极低 | 中高 |
选型建议很简单:存量项目里Moment代码多、时间紧,选Day.js,配合官方提供的迁移指南,全局替换加上插件注册基本一个下午就能搞定。新项目、团队偏好函数式风格、对包体积锱铢必较,选date-fns。需要重度时区运算的业务(比如跨国会议排期),Day.js加上utc和timezone插件是三者中体验最完整的。
最后补充一点迁移的实操经验:可以先用moment和dayjs在同一组件里共存的方式渐进迁移,每替换一个文件就删掉对应的moment引用,等引用数为零后再从package.json里移除依赖。date-fns迁移则建议按功能逐个改写,重点检查所有格式化字符串的大小写。无论选哪个,都不要再给新项目引入Moment.js了,这已经是前端社区的共识。