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

Spring Boot+Vue.js加载大JSON内存溢出及分页方案问询

大JSON文件流式传输内存溢出与前端分页解决方案

问题背景

使用Spring Boot以TEXT_EVENT_STREAM流式传输210MB以上的JSON文件到Vue.js前端时,后端触发Java堆内存溢出错误,前端加载全量数据后出现严重卡顿,需解决内存占用问题并实现分页展示。

问题根源分析

  1. 后端:原代码直接按字节流转发整个JSON文件,Spring容器可能会缓冲响应内容,导致大文件全部加载到内存引发溢出;且未对JSON结构做流式解析,无法实现逐对象输出。
  2. 前端:使用axios一次性接收全量响应并存入数组,渲染大量DOM节点导致页面卡顿。

后端优化:解决内存溢出

采用Jackson流式API解析JSON数组,逐个输出对象作为SSE事件,避免全量数据加载到内存,同时禁用响应缓冲。

修改后的Spring Boot代码

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;

@RestController
@RequestMapping("/apii")
public class DataController {

    @RequestMapping(value = "/getData", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public void getData(HttpServletResponse response) throws IOException {
        // 配置SSE响应头,禁用缓存,保持连接
        response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
        response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
        response.setHeader(HttpHeaders.CONNECTION, "keep-alive");

        File jsonFile = new File("My_Data.json");
        JsonFactory factory = new JsonFactory();
        // 流式解析JSON,自动关闭资源
        try (JsonParser parser = factory.createParser(jsonFile);
             var writer = response.getWriter()) {

            // 跳过JSON数组起始标记 [
            if (parser.nextToken() == JsonToken.START_ARRAY) {
                // 遍历数组内的每个对象
                while (parser.nextToken() != JsonToken.END_ARRAY) {
                    // 读取单个JSON对象并转为字符串
                    String jsonObject = parser.readValueAsTree().toString();
                    // 按SSE格式输出单个对象,每个事件单独换行
                    writer.write("data: " + jsonObject + "\n\n");
                    // 强制刷新输出流,避免容器缓冲
                    writer.flush();
                }
            }
        }
    }
}

前端优化:流式接收+分页展示

改用EventSource接收SSE流式数据,逐对象存储;同时实现分页逻辑,仅渲染当前页数据,减少DOM节点数量。

修改后的Vue.js代码

export default {
  data() {
    return {
      currentItems: [], // 当前页展示的数据
      allReceivedItems: [], // 存储已接收的所有数据
      currentPage: 1,
      pageSize: 50, // 每页展示50条
      eventSource: null
    };
  },
  mounted() {
    this.initSSEConnection();
  },
  beforeUnmount() {
    // 组件销毁前关闭SSE连接
    if (this.eventSource) {
      this.eventSource.close();
    }
  },
  methods: {
    initSSEConnection() {
      this.eventSource = new EventSource('http://localhost:8080/apii/getData');
      
      // 接收单个SSE事件
      this.eventSource.onmessage = (event) => {
        const item = JSON.parse(event.data);
        this.allReceivedItems.push(item);
        // 更新当前页数据
        this.refreshCurrentPage();
      };

      // 处理连接错误
      this.eventSource.onerror = (error) => {
        console.error('SSE连接异常:', error);
        this.eventSource.close();
      };
    },
    refreshCurrentPage() {
      const startIndex = (this.currentPage - 1) * this.pageSize;
      const endIndex = startIndex + this.pageSize;
      this.currentItems = this.allReceivedItems.slice(startIndex, endIndex);
    },
    // 切换到指定页
    switchPage(pageNum) {
      this.currentPage = pageNum;
      this.refreshCurrentPage();
    },
    // 上一页
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
        this.refreshCurrentPage();
      }
    },
    // 下一页
    nextPage() {
      const totalPages = Math.ceil(this.allReceivedItems.length / this.pageSize);
      if (this.currentPage < totalPages) {
        this.currentPage++;
        this.refreshCurrentPage();
      }
    }
  },
  computed: {
    totalPages() {
      return Math.ceil(this.allReceivedItems.length / this.pageSize);
    }
  }
};

修改后的前端展示代码(带分页控件)

<div class="data-container">
  <table class="content-table">
    <thead>
      <tr>
        <th>Data1</th>
        <th>Data2</th>
        <th>Data3</th>
        <th>Data4</th>
        <th>Data5</th>
        <th>Data6</th>
        <th>More</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="item in currentItems" :key="item.id">
        <td>{{ item.Data1 }}</td>
        <td>{{ item.Data2 }}</td>
        <td>{{ item.Data3 }}</td>
        <td>{{ item.Data4 }}</td>
        <td>{{ item.Data5 }}</td>
        <td>{{ item.Data6 }}</td>
        <td>
          <button @click="afficher(item)">
            <a href="/Detail">...</a>
          </button>
        </td>
      </tr>
    </tbody>
  </table>

  <!-- 分页操作栏 -->
  <div class="pagination-bar">
    <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
    <span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
    <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
  </div>
</div>

额外优化建议

  • 后端:若JSON文件超大型(千万级数据),建议将数据存入数据库,通过分页查询接口替代文件流式读取,进一步降低内存占用。
  • 前端:数据量达百万级时,推荐使用虚拟滚动组件(如vue-virtual-scroller),仅渲染当前视口内的DOM节点,彻底解决页面卡顿问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 05:32:43