iPhone端MUI预约日期时间选择器切换卡顿问题求解
问题描述
该问题仅在iPhone设备复现,Android设备无异常:在预约日期时间功能模块中,点击日历图标唤起选择器后,切换日期/时间值时页面出现明显卡顿。项目基于Metronic模板开发,日期时间选择功能相关核心实现代码如下:
核心实现代码
1. 日期时间选择组件代码
<Form.Group> <Form.Label> <FormattedMessage id="GENERAL.DATE_TIME_APPOINTMENT" /> </Form.Label> <MuiPickersUtilsProvider utils={MomentUtils}> <KeyboardDateTimePicker className="form-control" value={formatDateTimeStringAsDateTime(selectedDate)} onChange={handleDateChange} name="appointmentDate" minDate={new Date()} format={SAASDateTimeFormat} margin="dense" /> </MuiPickersUtilsProvider> </Form.Group>
2. MomentUtils工具类实现代码
export default class MomentUtils implements IUtils<defaultMoment.Moment> { moment: typeof defaultMoment; locale?: string; yearFormat: string; yearMonthFormat: string; dateTime12hFormat: string; dateTime24hFormat: string; time12hFormat: string; time24hFormat: string; dateFormat: string; constructor({ locale, instance, moment }?: Opts); parse(value: string, format: string): defaultMoment.Moment; date(value?: any): defaultMoment.Moment; isValid(value: any): boolean; isNull(date: Moment): boolean; getDiff(date: Moment, comparing: Moment | string): number; isAfter(date: Moment, value: Moment): boolean; isBefore(date: Moment, value: Moment): boolean; isAfterDay(date: Moment, value: Moment): boolean; isBeforeDay(date: Moment, value: Moment): boolean; isBeforeYear(date: Moment, value: Moment): boolean; isAfterYear(date: Moment, value: Moment): boolean; startOfDay(date: Moment): defaultMoment.Moment; endOfDay(date: Moment): defaultMoment.Moment; format(date: Moment, formatString: string): string; formatNumber(numberToFormat: string): string; getHours(date: Moment): number; addDays(date: Moment, count: number): defaultMoment.Moment; setHours(date: Moment, count: number): defaultMoment.Moment; getMinutes(date: Moment): number; setMinutes(date: Moment, count: number): defaultMoment.Moment; getSeconds(date: Moment): number; setSeconds(date: Moment, count: number): defaultMoment.Moment; getMonth(date: Moment): number; isSameDay(date: Moment, comparing: Moment): boolean; isSameMonth(date: Moment, comparing: Moment): boolean; isSameYear(date: Moment, comparing: Moment): boolean; isSameHour(date: Moment, comparing: Moment): boolean; setMonth(date: Moment, count: number): defaultMoment.Moment; getMeridiemText(ampm: "am" | "pm"): "AM" | "PM"; startOfMonth(date: Moment): defaultMoment.Moment; endOfMonth(date: Moment): defaultMoment.Moment; getNextMonth(date: Moment): defaultMoment.Moment; getPreviousMonth(date: Moment): defaultMoment.Moment; getMonthArray(date: Moment): defaultMoment.Moment[]; getYear(date: Moment): number; setYear(date: Moment, year: number): defaultMoment.Moment; mergeDateAndTime(date: Moment, time: Moment): defaultMoment.Moment; getWeekdays(): string[]; isEqual(value: any, comparing: any): boolean; getWeekArray(date: Moment): defaultMoment.Moment[][]; getYearRange(start: Moment, end: Moment): defaultMoment.Moment[]; getCalendarHeaderText(date: Moment): string; getYearText(date: Moment): string; getDatePickerHeaderText(date: Moment): string; getDateTimePickerHeaderText(date: Moment): string; getMonthText(date: Moment): string; getDayText(date: Moment): string; getHourText(date: Moment, ampm: boolean): string; getMinuteText(date: Moment): string; getSecondText(date: Moment): string; }
3. 日期变更处理函数代码
const handleDateChange = (e) => { setSelectedDate(e); setApptCheckingStartDate(e); setEndTime(e); setDurationHour("0"); setDurationMinute("0"); };
补充说明:
selectedDate状态的初始值为当日日期。
卡顿根因
- iOS Safari/Webview对Moment.js的可变对象操作、高频日期计算性能表现远差于Android端内核。每次切换选择器值时,MomentUtils会触发大量重复的日期格式化、大小比较、周/月日历数组生成计算,直接阻塞主线程导致掉帧卡顿。
- 组件传入的
minDate={new Date()}为JSX内联创建的对象,每次组件重渲染都会生成新的引用,触发日期选择器不必要的全量重绘。 value属性传入的是formatDateTimeStringAsDateTime(selectedDate)的即时执行结果,每次组件渲染都会重新跑一遍格式化逻辑,没有做结果缓存,存在大量无意义的重复计算开销。- 日期变更时一次性同步触发4个独立状态的更新,引发组件树多次串行重渲染,叠加Metronic模板本身的全局样式重计算逻辑,在iOS性能偏弱的设备上卡顿感会被进一步放大。
可落地方案
替换日期工具依赖
Moment.js早已停止官方维护,其可变对象设计、冗余计算逻辑在iOS端性能表现极差。替换为轻量不可变的Day.js或date-fns,配合对应的@date-io/dayjs/@date-io/date-fns适配器给MuiPickers组件使用,可减少70%以上的日期工具包体积,日期计算速度提升2-5倍。固定静态属性引用,消除无效重渲染
将内联创建的最小可选日期提到组件外部作为常量定义,避免每次渲染生成新引用:
// 组件作用域外定义,全局仅初始化一次 const MIN_SELECTABLE_DATE = new Date()
组件props中直接传入minDate={MIN_SELECTABLE_DATE}即可。
- 缓存选择器value计算结果
使用useMemo缓存格式化后的日期值,仅当依赖的selectedDate变化时才重新计算:
const pickerValue = useMemo(() => { return formatDateTimeStringAsDateTime(selectedDate) }, [selectedDate])
组件props中传入value={pickerValue}替换原来的即时计算值。
- 合并关联状态更新,减少重渲染次数
将强关联的日期相关状态合并为单个状态对象,一次更新完成,避免多次setState触发的串行重渲染:
// 替换原来分散的多个独立状态 const [apptDateConfig, setApptDateConfig] = useState({ selectedDate: initialDate, apptCheckingStartDate: initialDate, endTime: initialDate, durationHour: "0", durationMinute: "0" }) const handleDateChange = (e) => { setApptDateConfig(prev => ({ ...prev, selectedDate: e, apptCheckingStartDate: e, endTime: e, durationHour: "0", durationMinute: "0" })) }
- 关闭非必要动画降低渲染压力
iOS端Web内核对复杂DOM动画的帧率表现较差,可给KeyboardDateTimePicker添加animationDuration={0}属性,关闭弹窗弹出、值切换的过渡动画,能明显降低操作时的掉帧感。
内容的提问来源于stack exchange,提问作者Fatt Sky
相关产品推荐
相关产品推荐

