Django视图加载缓慢原因排查:含千条datalist的模态窗
Django视图与模态窗加载缓慢问题分析及优化方案
问题场景
你编写的add_item_in_release视图及对应Bootstrap模态窗模板调用时加载耗时长达10秒,但其他页面加载正常,怀疑是否由datalist包含约1000个选项导致。
视图代码
@permission_required('storage.add_iteminrelease') @transaction.atomic def add_item_in_release(request, id): release = get_object_or_404(Release, pk=id) if (request.method == 'POST'): add_item_in_release_form = ItemInReleaseForm(request.POST) if add_item_in_release_form.is_valid(): add_item_in_release_form.instance.release = release item = add_item_in_release_form.cleaned_data['item'] if item.count < add_item_in_release_form.cleaned_data['count']: messages.add_message(request, messages.ERROR, 'Кол-во выдачи не должно превышать ' + str(item.count)) return redirect('/releases/details/' + str(id) + '/') item.count -= add_item_in_release_form.cleaned_data['count'] item.save() item_in_release, create = ItemInRelease.objects.get_or_create(item=add_item_in_release_form.cleaned_data['item'], release=release, defaults={'count': add_item_in_release_form.cleaned_data['count']}) if not create: item_in_release.count += add_item_in_release_form.cleaned_data['count'] item_in_release.save() messages.add_message(request, messages.SUCCESS, 'Позиция успешно добавлена') return redirect('/releases/details/' + str(id) + '/') messages.add_message(request, messages.ERROR, add_item_in_release_form.errors.as_data()) return redirect('/releases/details/' + str(id) + '/') else: add_item_in_release_form = ItemInReleaseForm() return render(request, 'add_item_in_release.html', {'add_item_in_release_form': add_item_in_release_form, 'id': id})
模板代码
<div class="modal fade" id="add-item" tabindex="-1" aria-labelledby="add-item-label" aria-hidden="true" dats-bs-focus="false"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="add-item-label">Добавление позиции</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <form action="{% url 'storage:add-item-in-release' id %}" method="POST" class="form" novalidate> <div class="modal-body"> {% csrf_token %} <div class="form-floating w-100 mb-3"> {{ add_item_in_release_form.contract }} <label for="count">Договор</label> </div> <div class="input-group flex-nowrap mb-3"> <div class="form-floating w-100"> <input class="form-control" id="item-datalist" placeholder="Позиция" name="item" type="text" list="items" autocomplete="off"> <datalist id="items"> {% for i in add_item_in_release_form.fields.item.queryset %} <option data-value="{{ i.id }}" data-count="{{ i.count }}" data-contract="{{ i.contract }}" value="{{ i.item.manufacturer.name }} {{ i.item.article }} {{ i.contract }}"></option> {% endfor %} </datalist> <label for="item-datalist">Позиция</label> </div> <button class="btn btn-outline-secondary" type="button" id="clear-input" title="Очистить"> <i class="fa fa-eraser"></i> </button> </div> <div class="input-group flex-nowrap mb-3"> <div class="form-floating w-100"> {{ add_item_in_release_form.count }} <label for="count">Кол-во</label> </div> <span class="input-group-text w-25" id="count-label" title="Максимальное кол-во"></span> </div> </div> <div class="modal-footer"> <button type="submit" class="btn btn-outline-success">Добавить</button> <button type="button" class="btn btn-outline-primary" data-bs-dismiss="modal">Отменить</button> </div> </form> </div> </div> </div>
核心原因分析
你的猜测完全正确,1000个选项的datalist是加载缓慢的核心原因,具体体现在:
- 数据库N+1查询问题:模板中遍历
add_item_in_release_form.fields.item.queryset时,每个Item都要额外查询关联的manufacturer数据,导致数据库请求量暴增,后端响应延迟。 - 前端渲染压力:1000个
<option>标签会大幅增加HTML体积,浏览器解析、渲染DOM的时间显著变长。 - 未优化的查询集:表单初始化时直接拉取全量Item数据,没有做任何过滤或预加载,加重了数据库和网络传输负担。
优化方案
1. 后端优化数据库查询
在ItemInReleaseForm的初始化方法中,用select_related预加载关联对象,避免N+1查询:
class ItemInReleaseForm(forms.ModelForm): class Meta: model = ItemInRelease fields = ['contract', 'item', 'count'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 预加载关联的manufacturer数据,减少数据库查询次数 self.fields['item'].queryset = Item.objects.select_related('item__manufacturer').all() # 可选:只保留有库存的Item,进一步减少数据量 # self.fields['item'].queryset = self.fields['item'].queryset.filter(count__gt=0)
2. 替换datalist为异步搜索组件
1000个选项的datalist用户体验极差,建议用异步搜索替代:
- 移除模板中的datalist,保留输入框;
- 监听输入框输入事件,通过AJAX请求后端接口,根据关键词动态返回匹配的Item数据;
- 用Bootstrap Autocomplete、Select2等组件展示搜索结果,用户选择后将Item ID填充到隐藏字段中。
3. 预处理模板数据(保留datalist的折中方案)
如果必须保留datalist,在视图中提前处理好选项所需字段,避免模板中频繁访问关联对象:
else: add_item_in_release_form = ItemInReleaseForm() # 预加载并处理Item数据,直接生成模板需要的文本 items = Item.objects.select_related('item__manufacturer').values( 'id', 'count', 'contract', 'item__manufacturer__name', 'item__article' ) item_options = [] for item in items: display_text = f"{item['item__manufacturer__name']} {item['item__article']} {item['contract']}" item_options.append({ 'id': item['id'], 'count': item['count'], 'contract': item['contract'], 'display': display_text }) return render(request, 'add_item_in_release.html', {'add_item_in_release_form': add_item_in_release_form, 'id': id, 'item_options': item_options})
模板中遍历预处理好的数据:
<datalist id="items"> {% for opt in item_options %} <option data-value="{{ opt.id }}" data-count="{{ opt.count }}" data-contract="{{ opt.contract }}" value="{{ opt.display }}"></option> {% endfor %} </datalist>
4. 其他细节优化
- 用
reverse函数替代硬编码URL,提升代码可维护性:return redirect(reverse('storage:release-details', args=[id])) - 表单验证失败时,返回表单页面并保留错误信息,避免用户重复输入。
内容的提问来源于stack exchange,提问作者Sigarev Andrey
相关产品推荐
相关产品推荐

