优化多房间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"/> × {{ 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

