纯静态 + Web Worker + 虚拟滚动:我是怎么让浏览器吃下 10MB JSON 不卡的

DevFormatLab:一个把所有计算都关进 Web Worker 的离线工具箱

一、起因:一次安全告警

每天打开工位的第一件事,是粘一段线上日志到某个 JSON 格式化网站。

直到有一天,安全部门在群里通报:某同事把带 access_token 的请求 body 贴进了一个 .top 域名的格式化站,被出口流量审计抓到,走了一遍合规流程。

那一刻我意识到两件事:

  1. 主流"野生工具站"对企业合规几乎都是不合格的------轻则记访问日志,重则后端落库;
  2. 即便是合规的站,遇到几 MB 的日志也大概率把浏览器卡到无响应。

周末我用 Next.js + Web Worker 攒了一个纯静态、可离线、把所有重活都关在 Worker 里的工具箱:DevFormatLab。这篇文章不吹效果,只复盘里面的几个技术选择,以及踩过的坑。


二、为什么是 Next.js Static Export

需求很明确:没有业务后端、没有数据库,只用静态 CDN 分发。这样在物理上断绝了"用户数据被服务端记录"的路径------因为根本没有服务端可以记录。

Next.js 的 App Router + output: 'export' 是目前体验最好的纯静态方案:能用上 React Server Components 的预渲染、文件路由、<Image> 优化,最终产物又是一堆纯 HTML/JS/CSS。

java 复制代码
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  images: { unoptimized: true }, // 静态导出下 next/image 无法用默认 loader
  trailingSlash: true,           // Cloudflare Pages / GitHub Pages 上更稳
};
module.exports = nextConfig;

产物丢到 Cloudflare Pages,全球 CDN 命中,冷启动几十毫秒。带宽和运维成本接近为零,作为非盈利项目刚好。

需要诚实承认的一点 :用户访问时拉到的 JS 仍然来自我的 CDN,理论上我可以"偷偷"在某个版本里加追踪。所以信任根其实还是落在「代码开源 + 你可以自己 fork 后部署到自己的 Cloudflare」上------这一点会放在仓库 README 里写明。


三、真正的性能瓶颈是什么

写 Worker 之前,先得搞清楚瓶颈到底在哪。我用 Chrome Performance 把 10MB JSON 走一遍流程录下来,结果是这样的:

阶段 主线程耗时 占比
JSON.parse ~80ms 5%
JSON.stringify(obj, null, 2) ~120ms 8%
语法高亮 tokenize ~600ms 40%
DOM 节点挂载 ~700ms 47%

也就是说------JSON.parse 根本不是大头,高亮 + DOM 渲染才是真凶 。很多文章上来就把 JSON.parse 扔进 Worker,其实优化的是个寂寞。

明白这点后,方案就清晰了:

  1. 能放 Worker 的纯计算全放进去:parse、stringify、tokenize、CSV/JSON 互转、diff;
  2. DOM 渲染必须留在主线程,但用虚拟滚动把节点数从 5w+ 砍到 30 个;
  3. 大字符串传递用 Transferable Objects,绕过结构化克隆的拷贝开销。

四、Worker 通信:别忽略 Transferable

大数据 postMessage 默认走「结构化克隆」,意味着 10MB 的字符串要被完整复制一份------主线程一份、Worker 一份,瞬时内存翻倍,序列化本身也耗时。

字符串本身没法 transfer,但底层 ArrayBuffer 可以 。所以我把入口数据先编码成 Uint8Array,转移所有权过去,Worker 里再解码:

typescript 复制代码
// main.ts
const worker = new Worker(new URL('./formatter.worker.ts', import.meta.url), {
  type: 'module',
});

export function formatJson(raw: string): Promise<string> {
  const buf = new TextEncoder().encode(raw).buffer;
  return new Promise((resolve, reject) => {
    const onMessage = (e: MessageEvent) => {
      worker.removeEventListener('message', onMessage);
      e.data.error ? reject(new Error(e.data.error)) : resolve(e.data.result);
    };
    worker.addEventListener('message', onMessage);
    // 第二个参数:把 buf 的所有权转移到 worker,零拷贝
    worker.postMessage({ type: 'FORMAT_JSON', buf }, [buf]);
  });
}
typescript 复制代码
// formatter.worker.ts
self.onmessage = (e: MessageEvent) => {
  const { type, buf } = e.data;
  if (type !== 'FORMAT_JSON') return;
  try {
    const raw = new TextDecoder().decode(buf);
    const result = JSON.stringify(JSON.parse(raw), null, 2);
    self.postMessage({ result });
  } catch (err) {
    self.postMessage({ error: (err as Error).message });
  }
};

实测 10MB 输入下,主线程从 200ms 阻塞降到 <5ms。代价是 Worker 内存峰值会高一点,但用户感知层面已经完全无卡顿。

进阶选项是 SharedArrayBuffer,可以做到真正零拷贝双向共享,但需要部署侧配 Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp,对 Cloudflare Pages 这种纯静态托管来说配置成本偏高,目前没启用。


五、虚拟滚动:5w 行 DOM → 30 个 DOM

格式化后的 JSON 一旦展开有几万行,常规做法是一次性渲染所有 <div>,瞬间几百 MB 内存、滚动卡到怀疑人生。

最终用了 react-virtuoso,可视区外的行完全不挂载。配合 Worker 提前 tokenize 好的结果,列表项只做纯渲染,不再做任何字符串处理:

ini 复制代码
<Virtuoso
  data={tokenizedLines}            // Worker 输出的扁平化行数组
  itemContent={(_, line) => <HighlightedLine tokens={line} />}
  overscan={400}                   // 上下多渲染 400px,避免快速滚动出现白屏
  computeItemKey={(i) => i}
/>

这一步是整个项目里 ROI 最高的优化,单它一项就把"能用 / 不能用"的分水岭从几千行拉到了几十万行。


六、PWA:拔网线也能用

纯静态站接 PWA 几乎零成本。我用了 next-pwa,它内部走的是 Workbox,预缓存策略对 Next 静态产物开箱即用:

php 复制代码
// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development',
  register: true,
  skipWaiting: true,
});

module.exports = withPWA({
  output: 'export',
  images: { unoptimized: true },
  trailingSlash: true,
});

再补一份 public/manifest.json 声明应用元信息。第一次访问后整站会被 Service Worker 静默缓存到本地,之后即便断网,JSON 格式化、JWT 解码、Base64 转换全都照常工作------因为这些功能本来就不需要网络。

可以做个验证:装上之后开飞行模式刷新一下,整站照常打开。


七、踩过的坑

按踩坑严重程度列几个,给后来人避雷:

  1. next/image 在 static export 下默认 loader 直接报错 :必须 images.unoptimized = true 或自己写 loader。
  2. Worker 里别用 window / document :构建时不会报错,运行时直接 ReferenceError。涉及到的第三方库要确认它能在 Web Worker 环境跑。
  3. new URL('./worker.ts', import.meta.url) 这种写法是 webpack/turbopack 识别 Worker 入口的"魔法" ,不能用变量拼,否则编译时找不到。
  4. Service Worker 的更新策略skipWaiting: true 配合用户当前打开的页面会强制刷新,对工具站可以接受;如果是文档站要慎用,体验会很奇怪。
  5. JWT 解码这种功能千万别"顺手"加一个"在线验签"的便利按钮:一旦验签就需要密钥,密钥要走网络发出去就破坏了整个"本地优先"承诺。一开始我差点犯这个错。

八、目前的状态

第一阶段上线的功能(全部在本地完成,不发起任何业务请求):

  • JSON 格式化 / 压缩 / 错误位置定位 / 双栏 diff
  • JWT 本地解码(不验签,不联网)
  • URL / Base64 / HTML 编解码
  • CSV ↔ JSON 互转

🔗 在线地址:devformatlab.com

后续会加的:YAML ↔ JSON、正则可视化测试、cron 表达式解释器。如果你有想加的工具或者在使用中发现 bug,欢迎在评论区或者站内反馈页留言。

如果这套思路(纯静态 + Worker + 虚拟滚动 + PWA)对你做自己的开发者工具有一点帮助,那这篇文章就值了。


写在最后

JUST DO IT!

文章里所有性能数字都来自我本地 M1 Pro + Chrome 122 的实测,仅供参考;不同设备和浏览器结果会有差异。

相关推荐
Hilaku1 小时前
从搜索排名到 AI 回答? 先聊一聊 AI 可见度工具 BuildSOM !
前端·javascript·程序员
辰同学ovo1 小时前
用 Chrome DevTools MCP 给 AI 写的页面做“质检“
前端·人工智能·chrome devtools
乌托邦1 小时前
uni-mini-ci:让 uniapp 小程序构建后自动预览和上传
前端·vue.js·uni-app
豹哥学前端1 小时前
前端工程化实战:从包管理到 Vite 配置,一套下来全明白
前端·javascript·vite
星辰_mya1 小时前
彩云之上——[特殊字符]的架构师
java·后端·微服务·面试·架构
网安小白1 小时前
如果解决github域名解析问题
前端
葬送的代码人生2 小时前
用一句 Prompt 十分钟搓出完整应用
前端·html·ai编程
2601_954526752 小时前
逆向解析Temu底层动销算法:基于API高并发轮询与全域存量透视的自动化架构重构
算法·架构·自动化
ShiJiuD6668889992 小时前
大事件板块三
前端·bootstrap·html