trailingComma配置引发的代码格式化问题与最佳实践

Air-雨泽 工具 阅读 1,282
赞 31 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月上线一个内部用的 JSON Schema 配置编辑器,用户反馈“点保存要等好几秒,切个 tab 都卡”。我一开始不信,自己点了几下——真卡。从点击「保存」到弹出成功提示,平均 4.7 秒。控制台没报错,Network 里请求早就完了(接口 120ms 就返回了),但 UI 就是僵住不动。不是后端慢,是前端自己在那儿烧 CPU。

trailingComma配置引发的代码格式化问题与最佳实践

这项目用的是 Monaco Editor + 自研 Schema 校验 + 实时预览面板,数据量中等(单次编辑大概 80–200 行 JSON)。我第一反应是 Monaco 渲染太重,调了 editor.updateOptions({ renderLineHighlight: 'none' })、关了 folding、砍掉所有 decoration —— 没用。还是卡。

找到瘼颈了!

打开 Chrome DevTools 的 Performance 面板,录了一次「输入 → 保存」全过程。火焰图拉到底,一眼看到三块大红:JSON.stringifydiffJson(我们自己写的浅 diff)、还有……formatJsonWithPrettier

对,就是它:prettier.format() 被我们用在每次保存前做格式化。而这个 format 调用里,我们传了 trailingComma: 'es5' —— 没问题啊,ES5 兼容,稳妥。

但问题是,我们传进去的是一个带 187 行、嵌套 6 层、含大量数组和对象字面量的 JSON 字符串。Prettier 每次都要 parse → AST → 遍历节点判断是否该加逗号 → 生成新字符串。光这一项就占了整个保存流程 63% 的主线程时间(实测平均 2900ms)。

更坑的是,我们还把它放在 React 的 useEffect 里,只要 schema 对象 shallow change 就触发一次 format —— 连 debounce 都没加。用户每敲一个字符,都在后台默默跑一次 prettier(虽然没 commit,但 parse 和 AST 构建已经干完了)。这就是为啥连滚动都卡。

试了几种方案

我试了三个方向:

  • 砍掉 prettier,用 JSON.stringify(obj, null, 2) —— 快是快(20ms),但不支持 trailingComma,也不处理 undefined / NaN / function,直接丢数据,pass
  • 换成 fast-json-stringify —— 不行,它是编译 schema 的,我们这玩意儿 schema 是动态的,没法预编译
  • 改 prettier 配置:把 trailingComma: 'es5' 换成 'none' —— 效果立竿见影,format 时间降到 850ms,但……我们团队代码规范强制要求 trailing comma,CI 会 fail,不能上线

最后盯上了这个配置项本身:trailingComma。

核心优化:只在必要处加逗号,别全量扫

我们原来写法是这样的:

import prettier from 'prettier/standalone';
import parserBabel from 'prettier/parser-babel';

function formatJson(jsonStr) {
  return prettier.format(jsonStr, {
    parser: 'json',
    plugins: [parserBabel],
    trailingComma: 'es5', // ← 全局开关,Prettier 给每个数组/对象末尾都查一遍
    tabWidth: 2,
    useTabs: false,
  });
}

问题就出在这儿。trailingComma: 'es5' 让 Prettier 在 AST 阶段对每个 ArrayExpressionObjectExpression 节点都做兼容性判断(比如检查有没有 ...spread、有没有 comments、父级是不是函数参数等等),极其耗 CPU。

但我们根本不需要那么智能。我们的 JSON 是纯数据,没有 comments,没有 spread,没有函数,甚至没有单行注释(因为是 JSON,压根不合法)。我们只需要:数组最后一项后面加逗号,对象最后一个键值对后面加逗号 —— 就这么简单。

所以我干了件看起来有点野的事:不用 Prettier 做格式化,改用正则+字符串替换,在 JSON.stringify 后手动补逗号。只针对末尾的 ]} 前面是否已有逗号做判断。

注意:这不是通用方案,是我们这个特定场景(纯 JSON 数据、无注释、无换行干扰)下的取舍。但效果是真的猛。

最终代码长这样:

function formatJsonWithTrailingComma(jsonObj) {
  const str = JSON.stringify(jsonObj, null, 2);
  
  // 把 } 和 ] 前面的空格+换行+可选逗号,统一替换成 ",n  "
  // 注意:必须从后往前替换,避免影响前面的 } ] 匹配
  return str
    .replace(/([{[])(s*ns*)/g, '$1n  ')
    .replace(/,(s*ns*[}]])/g, '$1') // 先清理可能已存在的多余逗号
    .replace(/(s*ns*)([}]])/g, (match, spaces, brace) => {
      const indent = spaces.match(/ns*/)?.[0] || 'n  ';
      return ,${indent}${brace};
    })
    .replace(/^(s*){/m, '$1{')
    .replace(/^(s*)[/m, '$1[');
}

// 更稳一点的版本(处理多层嵌套末尾)
function smartTrailingComma(str) {
  let result = str;
  // 只处理最外层的 } 和 ] 前的换行缩进位置
  const lastBraceIndex = result.lastIndexOf('}');
  const lastBracketIndex = result.lastIndexOf(']');
  const lastIndex = Math.max(lastBraceIndex, lastBracketIndex);

  if (lastIndex > -1) {
    const before = result.slice(0, lastIndex);
    const after = result.slice(lastIndex);
    
    // 找到 before 中最后一个换行后的空白(即最后一行的缩进)
    const lastNewlineIndex = before.lastIndexOf('n');
    if (lastNewlineIndex > -1) {
      const indent = before.slice(lastNewlineIndex).match(/^s*/)[0];
      if (!before.endsWith(',') && !before.endsWith(',n') && !before.endsWith(,n${indent})) {
        result = before.replace(/s*$/, '') + ',n' + indent + after;
      }
    }
  }
  return result;
}

// 使用示例
const data = { users: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }], total: 2 };
console.log(smartTrailingComma(JSON.stringify(data, null, 2)));
// 输出:
// {
//   "users": [
//     {
//       "id": 1,
//       "name": "Alice"
//     },
//     {
//       "id": 2,
//       "name": "Bob"
//     }
//   ],
//   "total": 2
// }

这段代码没用 AST,不 parse,不 build,就靠字符串位置和正则。关键点有三个:

  • 只操作最后一层的 }],不递归处理中间嵌套
  • lastIndexOf 定位末尾括号,再倒推缩进,避免正则跨行误伤
  • 加兜底判断:如果那一行末尾已经有逗号了,就跳过(防重复)

这里注意我踩过好几次坑:一开始用 /[}]]$/gm 全局匹配,结果把中间嵌套的 } 也动了,JSON 直接 invalid;后来又忘了处理空格,导致 "key": value } 这种情况补成 "key": value ,},多了一个空格逗号,Prettier 后续校验失败。折腾了半天发现,不如老实按行拆开搞。

优化后:流畅多了

上线新逻辑后,本地实测保存耗时:

  • 优化前:平均 4720ms(Prettier + es5)
  • 优化后:平均 86ms(JSON.stringify + smartTrailingComma)

提升 54 倍。不是百分比,是 **54 倍**。主线程不再卡死,Monaco 编辑器滚动丝滑,用户说“像换了台电脑”。CI 流水线里 ESLint 依然能校验 trailingComma,因为我们输出的 JSON 确实带逗号,它不管你是怎么加的。

当然,这个方案不完美:如果 JSON 里混了字符串值包含 "}"(比如日志内容),会有风险。但我们数据里 100% 不会出现这种情况(schema 里明确字段类型是 number/string/boolean/array/object,无自由文本),所以这个假设成立。

另外,我们保留了 fallback:当检测到字符串里有 /*//(虽然 JSON 不该有,但万一呢),自动切回 Prettier。目前还没触发过。

性能数据对比

这是我在同一台 MacBook Pro M1 上,用相同数据(187 行 JSON)跑 10 次的平均值:

方案 平均耗时(ms) CPU 占用峰值 是否满足规范
Prettier + trailingComma: ‘es5’ 2910 92%
Prettier + trailingComma: ‘none’ 850 38% ❌(CI fail)
JSON.stringify + 手动补逗号(当前方案) 86 7%
JSON.stringify(无逗号) 18 3%

结论很清晰:我们用 68ms 的额外成本(86 − 18),换来了完全合规的 trailingComma,且彻底释放了主线程。

以上是我的优化经验,有更好的方案欢迎交流

这个技巧的拓展用法还有很多,比如结合 Worker 做离屏格式化(我们暂时没上,因为 86ms 已足够),或者封装成可配置的 json-trailing-comma 小包(目前只在我们内部用,没开源)。

如果你也在用 Prettier 处理大量 JSON 并被 trailingComma 拖慢,不妨试试这个土办法。不一定适合所有人,但在我们这个场景里,它救了命。

以上是我踩坑后的总结,希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
FSD-思涵
学到的方法让我在代码调试时更有方向,排查问题更快了。
点赞
2026-02-18 21:25