页面加载大量SQL数据时触发Out of memory 浏览器内存溢出错误
问题描述
批量Excel数据导入SQL数据库后,加载数据展示页面时浏览器提示out of memory错误,数据量约100行时无异常,错误提示如下:
现有实现存在三处核心问题导致内存溢出:
- 后端接口写法错误:当前Index接口返回的是
_db.Database整个数据库上下文对象,和视图绑定的IEnumerable<Employee>模型不匹配,会携带大量无用冗余数据参与视图渲染,额外占用网络传输和浏览器内存资源。 - 全量加载逻辑缺陷:当前实现将全量员工数据一次性从数据库查出,在服务端渲染成所有表格行的静态HTML返回给浏览器,DataTable初始化时还要解析全量DOM节点生成内部数据缓存。当数据量达到数千/数万行时,DOM节点数量、JS内存占用会超过浏览器单标签页内存阈值,触发内存溢出;100行数据量小,不会触发阈值因此运行正常。
- 视图DOM结构不规范:表头仅定义了3列,实际每行渲染了4列(3个数据列+1个操作列),浏览器渲染时会额外做兼容处理,增加不必要的内存开销。
根治方案:启用DataTable服务端分页
核心思路是不再一次性加载、渲染全量数据,每次只拉取当前页需要的少量数据,DOM节点永远只保留当前页内容,从根源上控制内存占用,支持十万级以上数据量无压力。
步骤1:修复后端基础查询逻辑
废弃返回_db.Database的错误写法,新增专门的分页查询接口供DataTable调用,接收分页参数,仅返回当前页需要的数据和总条数:
[HttpGet] public IActionResult GetEmployeePage(int draw, int start, int length) { // 统计总数据量 var totalCount = _db.Employees.AsNoTracking().Count(); // 分页取数,仅查询当前页需要的字段,关闭对象跟踪减少内存占用 var pageData = _db.Employees .OrderBy(e => e.id) .Skip(start) .Take(length) .Select(e => new { e.id, e.identifier, e.firstname, e.middlename, e.lastname }) .AsNoTracking() .ToList(); // 组装DataTable要求的返回格式,操作列HTML直接在后端拼接 var result = pageData.Select(e => new { e.firstname, e.middlename, e.lastname, operation = $@" <div class='form-button-action'> <button type='submit' class='btn btn-link btn-lg'> <a href='/Exposure/ViewDetails?Identifier={e.identifier}' class='fas fa-users' data-toggle='tooltip' data-placement='top' title='View Accumulation'></a> </button> <button type='submit' class='btn btn-link btn-lg' onclick='Edit({e.id})'> <a class='fas fa-pencil-alt' data-toggle='tooltip' data-placement='top' title='Edit Session'></a> </button> </div>" }); return Json(new { draw = draw, recordsTotal = totalCount, recordsFiltered = totalCount, data = result }); }
如果需要保留原Index页访问入口,修改Index接口逻辑为直接返回视图即可,不需要传全量数据:
public IActionResult Index() { return View(); }
步骤2:修改Razor视图,移除全量行渲染逻辑
删除原来遍历Model渲染所有行的代码,补全表头列,tbody留空交由DataTable动态填充:
@{ ViewData ["Title"] = "Index"; } <div class="table-responsive"> <table class="table text-center table-striped table-hover" id="basic-datatables" cellspacing="1"> <thead class="bg-dark text-light"> <tr> <th>First Name</th> <th>Middle Name</th> <th>Last Name</th> <th>Operation</th> </tr> </thead> <tbody></tbody> </table> </div>
步骤3:修改前端DataTable配置,开启服务端模式
<script> var table = $('#basic-datatables').DataTable({ searching: false, processing: true, serverSide: true, // 开启服务端分页核心开关 pageLength: 20, // 每页默认显示条数,可按需调整为10/50/100 oLanguage: { sEmptyTable: "No Record Found" }, ajax: { url: '/Employee/GetEmployeePage', // 替换为实际控制器路由 type: 'GET' }, columns: [ { data: 'firstname' }, { data: 'middlename' }, { data: 'lastname' }, { data: 'operation', orderable: false } // 操作列关闭排序 ] }); </script>
临时过渡方案(数据量<5000行场景)
如果数据量不大,暂时不想重构为服务端分页,可通过两个优化快速解决问题:
- 后端查询时关闭EF对象跟踪,仅返回需要的字段,禁止返回
_db.Database:
public IActionResult Index() { var employees = _db.Employees .Select(e => new Employee { id = e.id, identifier = e.identifier, firstname = e.firstname, middlename = e.middlename, lastname = e.lastname }) .AsNoTracking() .ToList(); return View(employees); }
- DataTable开启延迟渲染,初始化时仅创建当前页的DOM节点,不一次性生成所有行的DOM:
var table = $('#basic-datatables').DataTable({ searching: false, deferRender: true, // 延迟渲染开关 pageLength: 20, oLanguage: { sEmptyTable: "No Record Found" } });
注意:该方案仅支持5000行以内数据,数据量更大时必须使用服务端分页方案。
额外优化点
- 原代码中所有编辑按钮都使用了相同的
id="Edit",不符合HTML id唯一的规范,会导致JS事件异常,建议移除重复id,用class选择器绑定事件。 - 按钮嵌套
<a>标签的写法不符合HTML规范,建议直接用<a>标签做跳转,不需要套一层button。
内容的提问来源于stack exchange,提问作者Veck
相关产品推荐
相关产品推荐

