[{"data":1,"prerenderedAt":194},["ShallowReactive",2],{"article-large-json-rendering":3},{"article":4,"relatedArticles":147},{"slug":5,"category":6,"date":7,"readingMinutes":8,"tags":9,"relatedTool":15,"cn":16,"en":78,"author":135,"reviewedBy":138,"reviewedDate":140,"updatedDate":7,"sources":141,"noIndex":146},"large-json-rendering","性能优化","2026-07-28",13,[10,11,12,13,14],"JSON","Web Worker","虚拟列表","大文件","Vue 3","/tools/json/large-json-viewer",{"title":17,"description":18,"intro":19,"sections":20},"Web Worker 与虚拟列表渲染超大 JSON 的实现方法","拆解超大 JSON 在读取、解析、线程通信和 DOM 渲染中的内存与主线程瓶颈，并用 Web Worker 保留数据、按视口返回切片、Vue 3 虚拟列表渲染。","几十 MB 甚至更大的 JSON 文件会同时放大文件读取、UTF-8 解码、JSON.parse、对象内存、线程通信和 DOM 节点数量。Web Worker 只能把同步解析移出主线程，虚拟列表只能减少 DOM；两者都不能保证任意 100MB 文件一定可打开。本文给出一个可运行的浏览器端架构，并明确它适合什么数据、何时应改用流式解析或服务端处理。",[21,31,37,43,49,55,60,70],{"heading":22,"paragraphs":23,"bullets":26},"一、先区分四个瓶颈，避免只优化 JSON.parse",[24,25],"超大 JSON 查看器通常不是单点变慢。原始 ArrayBuffer、解码后的 JavaScript 字符串、解析后的对象以及用于展示的行数据可能同时存在；如果再把完整对象从 Worker 发回主线程，还会触发结构化克隆。","虚拟列表只控制可见 DOM 数量，不会自动降低解析对象的内存。Web Worker 只保证 UI 线程不执行解析，不会让解析本身变成流式，也不会减少总内存。",[27,28,29,30],"读取：File.arrayBuffer 会把文件内容载入内存。","解码：TextDecoder 把 UTF-8 字节转换为 JavaScript 字符串。","解析：JSON.parse 同步构造完整对象。","渲染：递归创建全部 DOM 会造成节点、布局和样式计算压力。",{"heading":32,"paragraphs":33,"code":36},"二、推荐架构：完整数据留在 Worker，主线程只拿可见切片",[34,35],"主线程把 File 转成 ArrayBuffer，并通过 transferable list 把 buffer 的所有权交给 Worker。ArrayBuffer 可以转移，普通 JavaScript 对象不能按同样方式零拷贝转移。","Worker 完成解码、解析和扁平化后保存 rows，只返回总行数。滚动时主线程发送 start/end，Worker 返回几十行可见数据。这样不会在初始化时克隆整棵对象到主线程。","// message types shared by the main thread and worker\nexport type MainToWorker =\n  | { type: 'load'; buffer: ArrayBuffer }\n  | { type: 'range'; start: number; end: number };\n\nexport type WorkerToMain =\n  | { type: 'ready'; total: number }\n  | { type: 'rows'; start: number; rows: JsonRow[] }\n  | { type: 'error'; message: string };\n\nexport type JsonRow = {\n  id: number;\n  depth: number;\n  key: string;\n  preview: string;\n  valueType: string;\n};",{"heading":38,"paragraphs":39,"code":42},"三、Worker：解码、解析并按范围返回数据",[40,41],"下面示例把 JSON 树扁平化为轻量行，用于演示固定行高虚拟列表。它仍然会保存完整解析对象和 rows，因此不是无限容量方案；对于极深对象、超大字符串字段或数百万节点，应使用增量解析器、懒展开索引或服务端预处理。","为了避免主线程收到完整对象，range 消息只返回当前窗口。preview 对长字符串进行截断，防止单个字段再次制造巨量 DOM 文本。","// json-viewer.worker.ts\ntype JsonRow = {\n  id: number;\n  depth: number;\n  key: string;\n  preview: string;\n  valueType: string;\n};\n\nlet rows: JsonRow[] = [];\nlet nextId = 0;\n\nfunction preview(value: unknown): string {\n  if (typeof value === 'string') {\n    return JSON.stringify(value.length > 200 ? `${value.slice(0, 200)}…` : value);\n  }\n  if (value === null) return 'null';\n  if (typeof value === 'object') {\n    return Array.isArray(value) ? `Array(${value.length})` : 'Object';\n  }\n  return String(value);\n}\n\nfunction flatten(value: unknown, key = '$', depth = 0): void {\n  rows.push({\n    id: nextId++,\n    depth,\n    key,\n    preview: preview(value),\n    valueType: value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value,\n  });\n\n  if (Array.isArray(value)) {\n    value.forEach((child, index) => flatten(child, `[${index}]`, depth + 1));\n  } else if (value && typeof value === 'object') {\n    for (const [childKey, childValue] of Object.entries(value)) {\n      flatten(childValue, childKey, depth + 1);\n    }\n  }\n}\n\nself.onmessage = (event: MessageEvent) => {\n  try {\n    if (event.data.type === 'load') {\n      rows = [];\n      nextId = 0;\n      const text = new TextDecoder('utf-8', { fatal: true }).decode(event.data.buffer);\n      const value: unknown = JSON.parse(text);\n      flatten(value);\n      self.postMessage({ type: 'ready', total: rows.length });\n      return;\n    }\n\n    if (event.data.type === 'range') {\n      const start = Math.max(0, event.data.start);\n      const end = Math.min(rows.length, event.data.end);\n      self.postMessage({ type: 'rows', start, rows: rows.slice(start, end) });\n    }\n  } catch (error) {\n    self.postMessage({ type: 'error', message: (error as Error).message });\n  }\n};",{"heading":44,"paragraphs":45,"code":48},"四、主线程：转移 ArrayBuffer，而不是发送完整字符串副本",[46,47],"File.arrayBuffer 得到的 buffer 在 postMessage 的第二个参数中被转移后，主线程一侧会失去对这块底层内存的访问权。这个行为是预期结果。","在 Nuxt 3 中创建 Worker 的代码必须只在客户端执行，例如放在 onMounted 内；不要在 SSR 阶段访问 Worker、File 或 window。","import { onBeforeUnmount, onMounted, ref } from 'vue';\n\nconst totalRows = ref(0);\nconst visibleRows = ref\u003CArray\u003C{\n  id: number;\n  depth: number;\n  key: string;\n  preview: string;\n  valueType: string;\n}>>([]);\n\nlet worker: Worker | undefined;\n\nonMounted(() => {\n  worker = new Worker(\n    new URL('./json-viewer.worker.ts', globalThis._importMeta_.url),\n    { type: 'module' },\n  );\n\n  worker.onmessage = (event) => {\n    if (event.data.type === 'ready') totalRows.value = event.data.total;\n    if (event.data.type === 'rows') visibleRows.value = event.data.rows;\n    if (event.data.type === 'error') console.error(event.data.message);\n  };\n});\n\nasync function loadFile(file: File): Promise\u003Cvoid> {\n  if (!worker) throw new Error('Worker is not ready');\n  const buffer = await file.arrayBuffer();\n  worker.postMessage({ type: 'load', buffer }, [buffer]);\n}\n\nonBeforeUnmount(() => worker?.terminate());",{"heading":50,"paragraphs":51,"code":54},"五、固定行高虚拟列表的核心计算",[52,53],"固定行高实现最简单：用总行数乘行高撑起滚动区域，再根据 scrollTop 计算起止索引，并增加 overscan 防止快速滚动时出现空白。只有 visibleRows 对应的节点进入 DOM。","如果内容允许自动换行，实际行高会变化，固定行高公式失效。大 JSON 查看器通常应禁止换行、截断预览，并在点击时单独展示完整值。","const rowHeight = 28;\nconst overscan = 8;\n\nfunction calculateRange(\n  scrollTop: number,\n  viewportHeight: number,\n  total: number,\n) {\n  const first = Math.floor(scrollTop / rowHeight);\n  const visibleCount = Math.ceil(viewportHeight / rowHeight);\n  const start = Math.max(0, first - overscan);\n  const end = Math.min(total, first + visibleCount + overscan);\n  return {\n    start,\n    end,\n    offsetY: start * rowHeight,\n    totalHeight: total * rowHeight,\n  };\n}",{"heading":56,"paragraphs":57,"code":59},"六、Vue 3 视口示例与请求节流",[58],"滚动事件中只更新范围，并在下一帧向 Worker 请求切片。若 start/end 没变化则不重复请求。模板中的 spacer 负责滚动条总高度，rows 容器通过 translateY 移动到正确位置。","\u003Ctemplate>\n  \u003Cdiv ref=\"viewport\" class=\"viewport\" @scroll=\"scheduleRange\">\n    \u003Cdiv :style=\"{ height: `${totalHeight}px` }\" />\n    \u003Cdiv class=\"rows\" :style=\"{ transform: `translateY(${offsetY}px)` }\">\n      \u003Cdiv\n        v-for=\"row in visibleRows\"\n        :key=\"row.id\"\n        class=\"row\"\n        :style=\"{ height: `${rowHeight}px`, paddingLeft: `${row.depth * 16}px` }\"\n      >\n        \u003Cstrong>{{ row.key }}\u003C/strong>: {{ row.preview }}\n      \u003C/div>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n.viewport { position: relative; height: 600px; overflow: auto; }\n.rows { position: absolute; inset: 0 0 auto 0; }\n.row { box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n\u003C/style>",{"heading":61,"paragraphs":62,"bullets":65},"七、内存边界与不适用场景",[63,64],"JSON.parse 需要完整文本，不能因为放进 Worker 就变成真正的流式解析。若浏览器需要同时保留字节、字符串、对象和行索引，峰值内存可能远大于文件大小。设备、浏览器、数据结构和字符串重复度都会影响结果，不能承诺“100MB 一定流畅”。","以下场景应考虑服务端分页、NDJSON、分块 API、专用流式 JSON 解析器或桌面应用：文件超过目标设备内存预算；只需要搜索少量字段；顶层数组可以天然分页；数据来自服务端且可改变协议；页面需要编辑而不只是查看。",[66,67,68,69],"不要把完整 parsed object 通过 postMessage 发回主线程。","不要同时长期保留原始文本、格式化文本、完整对象和完整 DOM。","不要对所有节点做语法高亮；只处理可见切片。","不要在每次 scroll 事件中同步执行大量计算或创建新 Worker。",{"heading":71,"paragraphs":72},"八、如何验证、FAQ 与结论",[73,74,75,76,77],"使用 Performance 面板观察主线程长任务；在 Worker 内分别记录 decode、parse、flatten 时间；用浏览器任务管理器或 Memory 工具观察峰值内存；滚动时确认 DOM 节点数量接近可见行加 overscan，而不是总行数。测试数据应包含大数组、深层对象、超长字符串和非法 UTF-8。","问：Worker 会让 JSON.parse 更快吗？答：不一定，它主要避免解析阻塞 UI，实际解析耗时仍取决于引擎和数据。","问：可以把 parsed object 设为 Transferable 吗？答：普通对象不可以；ArrayBuffer 等特定对象可转移，普通对象通过 structured clone 复制。","问：为什么虚拟列表后内存仍然高？答：它减少的是 DOM，完整字符串、对象和索引仍可能占用大量内存。","结论：可靠方案是转移输入 buffer、让完整数据留在 Worker、按视口返回行切片，并明确内存上限。语法失败应参考 /articles/json-unexpected-token；数字精度问题应参考 /articles/json-number-precision。",{"title":79,"description":80,"intro":81,"sections":82},"Rendering Large JSON with Web Workers and Virtual Lists","Separate file reading, UTF-8 decoding, JSON parsing, worker messaging, and DOM costs. Keep full data in a worker, return viewport slices, and render them with a Vue 3 fixed-height virtual list.","Large JSON files amplify several independent costs: reading bytes, decoding text, synchronous JSON.parse, object memory, worker messaging, and DOM creation. A worker moves CPU work away from the UI thread, while virtualization limits DOM nodes; neither guarantees that every 100 MB file will fit in memory. This guide implements a bounded browser architecture and explains when to switch to streaming or server-side processing.",[83,93,98,103,108,113,118,128],{"heading":84,"paragraphs":85,"bullets":88},"1. Identify the four bottlenecks",[86,87],"The raw ArrayBuffer, decoded JavaScript string, parsed object, flattened row index, and DOM can coexist. Sending the complete object from a worker to the main thread also invokes structured cloning.","Virtualization reduces DOM size, not parsed-object memory. A worker prevents main-thread parsing but does not make JSON.parse streaming or reduce total memory by itself.",[89,90,91,92],"File reading loads bytes into memory.","TextDecoder creates a JavaScript string.","JSON.parse synchronously constructs the full value.","Recursive rendering can create an excessive number of DOM nodes.",{"heading":94,"paragraphs":95,"code":97},"2. Architecture: retain full data in the worker",[96],"Transfer the file ArrayBuffer to the worker. ArrayBuffer is transferable; a normal JavaScript object is not zero-copy transferable in the same way. The worker owns the parsed data and row index, returns only the total count, and serves small start/end slices as the viewport changes.","export type MainToWorker =\n  | { type: 'load'; buffer: ArrayBuffer }\n  | { type: 'range'; start: number; end: number };\n\nexport type JsonRow = {\n  id: number;\n  depth: number;\n  key: string;\n  preview: string;\n  valueType: string;\n};",{"heading":99,"paragraphs":100,"code":102},"3. Worker implementation",[101],"This example flattens a tree into lightweight rows for a fixed-height viewer. It still stores the parsed value and all rows, so it is not an unlimited solution. Extremely deep trees, huge string fields, or millions of nodes need lazy indexing, incremental parsing, or server preprocessing.","// json-viewer.worker.ts\ntype JsonRow = {\n  id: number;\n  depth: number;\n  key: string;\n  preview: string;\n  valueType: string;\n};\n\nlet rows: JsonRow[] = [];\nlet nextId = 0;\n\nfunction preview(value: unknown): string {\n  if (typeof value === 'string') {\n    return JSON.stringify(value.length > 200 ? `${value.slice(0, 200)}…` : value);\n  }\n  if (value === null) return 'null';\n  if (typeof value === 'object') {\n    return Array.isArray(value) ? `Array(${value.length})` : 'Object';\n  }\n  return String(value);\n}\n\nfunction flatten(value: unknown, key = '$', depth = 0): void {\n  rows.push({\n    id: nextId++,\n    depth,\n    key,\n    preview: preview(value),\n    valueType: value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value,\n  });\n  if (Array.isArray(value)) {\n    value.forEach((child, index) => flatten(child, `[${index}]`, depth + 1));\n  } else if (value && typeof value === 'object') {\n    for (const [childKey, childValue] of Object.entries(value)) {\n      flatten(childValue, childKey, depth + 1);\n    }\n  }\n}\n\nself.onmessage = (event: MessageEvent) => {\n  try {\n    if (event.data.type === 'load') {\n      rows = [];\n      nextId = 0;\n      const text = new TextDecoder('utf-8', { fatal: true }).decode(event.data.buffer);\n      flatten(JSON.parse(text));\n      self.postMessage({ type: 'ready', total: rows.length });\n    } else if (event.data.type === 'range') {\n      const start = Math.max(0, event.data.start);\n      const end = Math.min(rows.length, event.data.end);\n      self.postMessage({ type: 'rows', start, rows: rows.slice(start, end) });\n    }\n  } catch (error) {\n    self.postMessage({ type: 'error', message: (error as Error).message });\n  }\n};",{"heading":104,"paragraphs":105,"code":107},"4. Transfer the input buffer from the client",[106],"After postMessage transfers the buffer, the main-thread buffer is detached. In Nuxt 3, create the worker only on the client, for example inside onMounted, because Worker, File, and window are not available during SSR.","import { onBeforeUnmount, onMounted } from 'vue';\n\nlet worker: Worker | undefined;\n\nonMounted(() => {\n  worker = new Worker(\n    new URL('./json-viewer.worker.ts', globalThis._importMeta_.url),\n    { type: 'module' },\n  );\n});\n\nasync function loadFile(file: File): Promise\u003Cvoid> {\n  if (!worker) throw new Error('Worker is not ready');\n  const buffer = await file.arrayBuffer();\n  worker.postMessage({ type: 'load', buffer }, [buffer]);\n}\n\nonBeforeUnmount(() => worker?.terminate());",{"heading":109,"paragraphs":110,"code":112},"5. Fixed-height virtualization math",[111],"Create a spacer with total * rowHeight, calculate the visible range from scrollTop, and add overscan. Disable wrapping and truncate previews; variable-height rows require a different measurement and caching strategy.","const rowHeight = 28;\nconst overscan = 8;\n\nfunction calculateRange(scrollTop: number, height: number, total: number) {\n  const first = Math.floor(scrollTop / rowHeight);\n  const count = Math.ceil(height / rowHeight);\n  const start = Math.max(0, first - overscan);\n  const end = Math.min(total, first + count + overscan);\n  return {\n    start,\n    end,\n    offsetY: start * rowHeight,\n    totalHeight: total * rowHeight,\n  };\n}",{"heading":114,"paragraphs":115,"code":117},"6. Vue viewport structure",[116],"Request a new worker slice only when start/end changes, ideally once per animation frame. The spacer provides the scrollbar, and the visible row container is translated to the correct offset.","\u003Ctemplate>\n  \u003Cdiv class=\"viewport\" @scroll=\"scheduleRange\">\n    \u003Cdiv :style=\"{ height: `${totalHeight}px` }\" />\n    \u003Cdiv class=\"rows\" :style=\"{ transform: `translateY(${offsetY}px)` }\">\n      \u003Cdiv\n        v-for=\"row in visibleRows\"\n        :key=\"row.id\"\n        class=\"row\"\n        :style=\"{ height: `${rowHeight}px`, paddingLeft: `${row.depth * 16}px` }\"\n      >\n        \u003Cstrong>{{ row.key }}\u003C/strong>: {{ row.preview }}\n      \u003C/div>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"heading":119,"paragraphs":120,"bullets":123},"7. Memory boundaries and alternatives",[121,122],"JSON.parse requires complete text. Peak memory can be much larger than the file because bytes, text, objects, and indexes coexist. Do not promise a universal file-size threshold.","Prefer server pagination, NDJSON, chunked APIs, a streaming parser, or a desktop process when the file exceeds the device budget, the top-level array is naturally pageable, or the user needs only a small subset of fields.",[124,125,126,127],"Do not postMessage the complete parsed object to the UI thread.","Do not keep raw, pretty-printed, parsed, and fully rendered copies indefinitely.","Highlight only visible rows.","Do not create a new worker for every scroll event.",{"heading":129,"paragraphs":130},"8. Verification, FAQ, and conclusion",[131,132,133,134],"Measure decode, parse, and flatten separately in the worker; inspect main-thread long tasks and peak memory; confirm that the DOM node count tracks the viewport rather than total rows. Include large arrays, deep objects, long strings, and invalid UTF-8 in tests.","Does a worker make JSON.parse faster? Not necessarily; it mainly keeps the UI responsive.","Can a parsed object be transferred? Normal objects are structured-cloned; specific types such as ArrayBuffer are transferable.","Why is memory still high after virtualization? The full text, parsed object, and row index still exist. The robust design transfers input bytes, retains data in the worker, and sends viewport slices only. See /articles/json-unexpected-token and /articles/json-number-precision for separate parsing concerns.",{"name":136,"url":137},"Tiny's Tool Editorial Team","https://tinystool.org/about",{"name":139,"url":137},"Tiny's Tool Technical Review","2026-08-08",[142],{"title":143,"url":144,"publisher":145},"RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format","https://www.rfc-editor.org/rfc/rfc8259","RFC Editor",false,[148,164,179],{"slug":149,"noIndex":150,"category":151,"date":7,"updatedDate":140,"reviewedDate":140,"readingMinutes":152,"tags":153,"relatedTool":157,"cn":158,"en":161},"code-diff-large-file-web-worker-dom-chunks",true,"最佳实践",8,[154,11,155,156],"Code Diff","Virtualization","Performance","/tools/dev/code-diff",{"title":159,"description":160},"万行大文件代码对比性能优化：Web Worker 异步计算与 DOM 虚拟化分片渲染","讲解在前端对比万行大文件时，如何使用 Web Worker 隔离 CPU 密集 Diff 计算，并结合 DOM 虚拟化视口 (Virtualization) 解决主线程卡死问题。",{"title":162,"description":163},"Optimizing Large File Code Diff Performance: Web Worker Computation and DOM Virtualization","Learn how to optimize web-based large file diffs by offloading CPU-bound tasks to Web Workers and applying DOM virtualization for zero UI lag.",{"slug":165,"noIndex":146,"category":166,"date":7,"updatedDate":7,"reviewedDate":140,"readingMinutes":152,"tags":167,"relatedTool":172,"cn":173,"en":176},"json-number-precision","踩坑避坑",[10,168,169,170,171],"JavaScript","Number","BigInt","精度丢失","/tools/json/json-validator",{"title":174,"description":175},"JSON 大整数精度丢失：Number 安全范围与解决方案","说明 JSON 大整数进入 JavaScript 后为什么会被舍入，演示 Number.MAX_SAFE_INTEGER 边界，并给出字符串契约、BigInt 转换和序列化的可验证方案。",{"title":177,"description":178},"Large Integers in JSON: JavaScript Precision Limits and Fixes","Learn why valid JSON integers can be rounded when parsed into JavaScript Number values, and apply string contracts, BigInt conversion, and safe serialization without losing digits.",{"slug":180,"noIndex":146,"category":181,"date":7,"updatedDate":7,"reviewedDate":140,"readingMinutes":182,"tags":183,"relatedTool":172,"cn":188,"en":191},"json-unexpected-token","错误排查",10,[10,184,185,186,187],"JSON.parse","SyntaxError","Unexpected token","调试",{"title":189,"description":190},"JSON Unexpected token：8 类原因与定位","从原始响应、错误位置和 JSON 语法三层定位 JSON.parse 的 Unexpected token，覆盖 HTML 响应、重复解析、尾逗号、引号、控制字符、非法数字和截断数据。",{"title":192,"description":193},"JSON.parse Unexpected Token: 8 Causes and a Reliable Debugging Flow","Debug JSON.parse syntax failures by inspecting the raw response, input type, error position, and JSON grammar. Covers HTML responses, double parsing, trailing commas, escaping, invalid numbers, and truncated payloads.",1786725508073]