You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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性能偏弱的设备上卡顿感会被进一步放大。
可落地方案
  1. 替换日期工具依赖
    Moment.js早已停止官方维护,其可变对象设计、冗余计算逻辑在iOS端性能表现极差。替换为轻量不可变的Day.js或date-fns,配合对应的@date-io/dayjs/@date-io/date-fns适配器给MuiPickers组件使用,可减少70%以上的日期工具包体积,日期计算速度提升2-5倍。

  2. 固定静态属性引用,消除无效重渲染
    将内联创建的最小可选日期提到组件外部作为常量定义,避免每次渲染生成新引用:

// 组件作用域外定义,全局仅初始化一次
const MIN_SELECTABLE_DATE = new Date()

组件props中直接传入minDate={MIN_SELECTABLE_DATE}即可。

  1. 缓存选择器value计算结果
    使用useMemo缓存格式化后的日期值,仅当依赖的selectedDate变化时才重新计算:
const pickerValue = useMemo(() => {
  return formatDateTimeStringAsDateTime(selectedDate)
}, [selectedDate])

组件props中传入value={pickerValue}替换原来的即时计算值。

  1. 合并关联状态更新,减少重渲染次数
    将强关联的日期相关状态合并为单个状态对象,一次更新完成,避免多次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"
  }))
}
  1. 关闭非必要动画降低渲染压力
    iOS端Web内核对复杂DOM动画的帧率表现较差,可给KeyboardDateTimePicker添加animationDuration={0}属性,关闭弹窗弹出、值切换的过渡动画,能明显降低操作时的掉帧感。

内容的提问来源于stack exchange,提问作者Fatt Sky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:48:20