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

优化多房间Vue日历输入渲染性能:解决3000输入渲染过慢问题

优化多房间周期日历的渲染性能

问题场景

这段代码用于构建多建筑多房间的周期日历(例如30天周期)。按此计算,单房间对应150个输入元素(30天×5类元素),若存在20个房间,将生成3000个输入,导致渲染速度极慢,请问是否有办法缩短渲染时间?

原代码

<div v-if="room.expanded">
  <div class="panel position-relative panel-content dates-panel">
    <div class="dates-header d-flex">
      <div class="dates-scroller" :key="`datesHeader`+room.id">
      </div>
      <div class="dates-container">
        <overlay-scrollbars
          :ref="`datesHeader`+room.id"
          :options="datesScrollOptions"
          :data-id="room.id"
        >
          <div class="dates d-flex">
            <div class="month" v-for="(month, i) in dates" :key="month.month+i">
              <span> {{ month.month }} </span>
              <div class="days d-flex">
                <div
                  class="day"
                  v-for="(day, index) in month.days"
                  :key="index+day.d+room.id"
                  :class="{'we': day.w>4, 'wee': day.w>5}"
                >
                  <span class="t">{{ day.t }}</span>
                  <span class="wd">{{ day.d }}</span>
                </div>
              </div>
            </div>
          </div>
        </overlay-scrollbars>
      </div>
    </div>
  </div>

  <div class="room-data d-flex">
    <div class="headers">
      <div class="header">{{ $t('calendar.roomStatus') }}</div>
      <div class="header">{{ $t('calendar.roomAvail') }}</div>
      <div class="header">{{ $t('calendar.roomPrice') }}
        <span v-if="room.occupancy.std <= 3">
          <icon width="11" height="11"  v-for="i in room.occupancy.std" :key="i+room.id" type="user"/>
        </span>
        <span v-else>
          <icon width="11" height="11"  :key="'icon'+i+room.id" type="user"/> &times; {{ room.occupancy.std }}
        </span>
        <span class="asset" v-html="currencySymbol()"></span>
      </div>
      <div
        class="header restriction"
        :class="{ 'd-none': !isRestrictionShown(rts.id), 'last-shown': lastShownRestriction(rts.id) }"
        v-for="(rts, i) in restrictionOptions"
        :key="rts.id + i">
        <span>{{ rts.text }}</span>
        <icon width="16" height="16" v-b-tooltip:page-calendar.right="rts.tip" type="info"/>
      </div>
    </div>
    <div class="rows" ref="roomRows">
      <div class="data room-status">
        <div class="day" v-for="(day,i) in data[room.id]"
             :key="day.i+i+room.id" :class="day.avail>0 && !day.csale ? 'bookable' : 'blocked'"></div>
      </div>
      <div class="data room-editable" data-field="avail">
        <div
          class="day"
          v-for="(day,i) in data[room.id]"
          @click.self="focusField($event)"
          :key="day.i+i+room.id"
          :class="day.avail>0 && !day.csale ? 'bookable' : 'blocked'"
        >
          <spinner v-if="isDayUpdating(room.id, day.i, 'avail')"/>
          <div v-else-if="isDayUpdated(room.id, day.i, 'avail')" class="day-updated">
            <icon width="18" height="18" type="ok-tick"/>
          </div>
          <b-input
            :value="day.avail" v-else
            @focus.prevent="selectField($event, day)"
            @mouseup.stop.prevent
            @blur="deselectField"
            @keyup.esc="resetField"
            @change.capture="updateField($event, room.id)"
          />
        </div>
      </div>
      <div class="data room-editable" data-field="price" :class="{ 'last-shown': !selectedRestrictions.length}">
        <div
          class="day"
          v-for="(day,i) in data[room.id]"
          @click.self="focusField($event)"
          :key="day.i+i"
          :class="day.avail>0 && !day.csale ? 'bookable' : 'blocked'"
        >
          <spinner v-if="isDayUpdating(room.id, day.i, 'price')"/>
          <div v-else-if="isDayUpdated(room.id, day.i, 'price')" class="day-updated">
            <icon width="18" height="18" type="ok-tick"/>
          </div>
          <b-input
            :value="day.price" v-else
            :disabled="day.avail==0"
            @focus.prevent="selectField($event, day)"
            @mouseup.stop.prevent
            @blur="deselectField"
            @keyup.esc="resetField"
            @change.capture="updateField($event, room.id)"
          />
        </div>
      </div>

      <div
        v-for="rts in restrictionOptions"
        :key="rts.id"
        class="data room-editable"
        :class="{ 'd-none': !isRestrictionShown(rts.id), 'last-shown': lastShownRestriction(rts.id) }"
        :data-field="rts.fld"
      >
        <div
          class="day"
          v-for="(day,i) in data[room.id]"
          @click.self="focusField($event)"
          :key="day.i+i+'day'"
          :class="day.avail>0 && !day.csale ? 'bookable' : 'blocked'"
        >
          <spinner v-if="isDayUpdating(room.id, day.i, rts.fld)"/>
          <div v-else-if="isDayUpdated(room.id, day.i, rts.fld)" class="day-updated">
            <icon width="18" height="18" type="ok-tick"/>
          </div>
          <template v-else>
            <b-input
              class="booking_slot"
              v-if="!rts.cb"
              :value="day[rts.fld]"
              :disabled="day.avail==0"
              @focus.prevent="selectField($event, day)"
              @mouseup.stop.prevent
              @blur="deselectField"
              @keyup.esc="resetField"
              @change.capture="updateField($event, room.id)"
            />
            <check-box
              v-else
              empty
              :value="day[rts.fld]"
              :disabled="day.avail==0"
              @input="updateField($event, room.id, day, rts.fld)"
            />
          </template>
        </div>
      </div>
    </div>
  </div>
</div>

优化方案

  • 虚拟滚动(Virtual Scrolling)
    只渲染当前视口内的房间和日期单元格,而非一次性加载全部元素。可借助Vue虚拟滚动类库实现,或自行监听滚动事件计算可见区域索引,仅渲染对应范围内的内容。日期横向滚动部分也可应用此逻辑,只加载当前可见的日期列。

  • 延迟渲染与懒加载
    初始仅渲染展开房间的内容,未展开房间(room.expanded为false)只保留基础信息,不渲染日历输入元素。用户展开房间时再动态加载对应组件,甚至可分步骤渲染:先显示日期表头,再异步渲染输入元素。

  • 组件拆分与缓存
    将重复的day单元格拆分为独立子组件,结合keep-alive缓存未修改的组件实例,减少重复渲染开销。同时确保子组件props使用不可变值,比如解构day属性后传递,避免因引用类型变化触发不必要更新。

  • 减少响应式计算开销
    把模板中的方法调用(如isDayUpdating、isDayUpdated)的结果提前缓存到data或computed中,比如用对象存储每个房间、日期、字段的更新状态,避免渲染时重复计算。同时将模板中的复杂表达式(如判断是否可预订)提前预处理为day对象的布尔属性,直接引用即可。

  • 优化Key值
    简化模板中的key值,比如将:key="day.i+i+room.id"改为:key="${room.id}-${day.i}",使用稳定唯一的标识,帮助Vue更高效地跟踪节点变化,减少DOM操作次数。

  • 简化输入组件
    非编辑状态下用普通文本元素替代b-input,仅在用户点击单元格时切换为输入组件。同时禁用b-input中不需要的特性(如自动完成、校验提示),减少组件内部渲染逻辑。

  • 数据预处理
    渲染前对data[room.id]进行预处理,提前计算好可预订状态、更新状态等需要在模板中使用的值并存储到数据对象中。同时过滤restrictionOptions中不需要显示的项,减少v-for循环次数,避免渲染隐藏元素。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 19:05:25