FLJ性能战争战报:完全抛弃 Next.js 打包链路,战术背断性选择 esbuild 自建 Worker 脚本经验

前言

连续忙了2个月aimuo v1.0 终于上线了,今天花点时间整理一下战斗笔记...

时间:2025年6月29日上午

战场:Text Format 核心组件渲染链路

模块:formatter.worker.ts + useFormatter.ts + dynamic + 动态导入


一、战斗统观

  • 目标:完全清除首层页 First Load JS (FLJ) 中所有大型格式化依赖
    • 包括 sql-formatter / js-yaml / js-beautify
    • 目标指标:Lighthouse Performance 达到88+ (mobile 预设)
  • 现状:
    • 重构 formatter 后已使用 Worker + await import()
    • 但被 Next.js 15 静态分析抓起全部格式化库
    • 无论是 dynamic / 条件渲染 / 动态导入都无法脱离 build 过程分析
  • 进行了数次试验后确诊:Next.js 15 与 webpack5 的打包分析链路非常深,worker 中的 static import 会被一路拉入 client.html chunk中

二、问题根源

环节 问题分析
new Worker(URL) 即使是事件驱动,依然被 Next.js 静态分析引入 initial chunk
import() 动态 Webpack 会把相关库提升到 vendors bundle
Worker 内 import 尽管动态,如果打包依赖路径是 static 字符串,仍被抓起

三、战术解决方案

完全抛弃 Next.js 的打包依赖分析链路

重点策略:用 esbuild 单独打包 Worker

  1. scripts/ 目录新建 generate-worker.js ,使用 CommonJS + esbuild 脚本打包 formatter.worker.ts

    • esbuild.build({ ... }) 指定 bundle / minify / esm
    • 输出为 public/workers/formatter.js
  2. Worker 中给格式化类型动态 import

    typescript 复制代码
    const loadFormatter = async (type: FormatType) => {
      switch (type) {
        case 'json': return (await import('../formatters/json')).format;
        case 'yaml': return (await import('../formatters/yaml')).format;
        case 'sql':  return (await import('../formatters/sql')).format;
        // ...
      }
    }
  3. 修改 useFormatter.ts ,直接指向 public Worker js 文件

    go 复制代码
    const worker = new Worker('/workers/formatter.js', { type: 'module' });
  4. 封装 shim :解决依赖库内部访问 process.xxx

    arduino 复制代码
    // process-shim.js
    export const process = { env: {}, argv: [], version: '' };

    并在 esbuild 打包时 inject: [path.resolve(__dirname, './process-shim.js')]

  5. alias 解决 DOM-only 版本库问题

    • decode-named-character-reference

    • 不能用 require.resolve(),会报 exports 错误

    • 直接:

      csharp 复制代码
      alias: {
        'decode-named-character-reference': path.join(
          projectRoot,
          'node_modules/decode-named-character-reference/index.js',
        )
      }

四、战果分析

指标改善

修改前 修改后
FLJ 大库总量 ~573 KB 0 KB
Performance 评分 (mobile) 红81-82 提升到88

工具验证

  • whybundled --chunks initial 确认 sql-formatterjs-yaml 不再存在
  • .next/analyze/client.html 最大的 initial chunk 不再包括 formatter 依赖
  • DevTools Network:首次点击时才进行 /workers/formatter.js 请求

五、战斗反思

错误往路 分析
dynamic() 动态切换 结果尽管是动态读取,但依然被分析链抓起
await import() 想着不会被打包输出,实际上如果在同一 chunk 链路上被使用,依然被拉进 vendors
Worker + static URL 不管如何动态渲染,Next.js 15 有能力精确分析 URL string 链路

本次战斗意义

  • 解锁 Next.js 15 下特定 npm 依赖无法分离 initial chunk 的困境
  • 策略上完全抽离应用系统打包链路,成功通过 CDN 进入 runtime

战斗资料

npx whybundled 不再出现这些格式化重型库

FLJ client.html size 从8.08MB → 7.47MB

后记

我们用一天半的时间,从无到有打通了 Next.js 应用中的一个重大性能阀,不仅是性能改善,更是有了对与 Next.js、esbuild、FLJ、动态 import 、worker、CDN 转移 等设计阅读重要概念的交集经验。

这是我们通往 95+ 评分的第一场难战,但也是最重要的基石之一。

截止到 aimuo 1.0 正式上线,我们已经从当初臃肿的8MB 直接干到了4MB

增加了一堆功能后,Lighthouse评分依然稳定在90+ .

顺嘴吐槽一句,Nextjs15 Turbopack 对worker的支持还是相当弱啊,加油兼容啊!


👉 实战项目 aimuo.com

有兴趣的小伙伴可以感受一下Nextjs15 App Router + Nestjs 架构的实战项目。

相关推荐
怕浪猫3 小时前
FDE 如何正确解决现场项目工程性的问题
前端·后端·ai编程
西瓜太郎12343 小时前
外层模型卡片与分组详情成功率不一致,应该怎么排查?
前端·typescript·go·软件工程·react
GreenTea3 小时前
vLLM 与 SGLang KV Cache 底层实现机制深度调研报告
前端·后端·算法
天天喝旺仔3 小时前
Go 并发编程:Goroutine 与 Channel 实战
云原生·性能优化·架构·go
几何心凉6 小时前
没有万能模型:我用 AiiOnly Token Plan,把多款大模型的长处拼进同一个项目
前端
kyriewen6 小时前
Claude Code 的额度今天缩水了17%——官方公告上写的是"永久提高25%"
前端·ai编程·claude
雪芽蓝域zzs7 小时前
vue解构平铺VS对象包裹
前端·javascript·vue.js
风骏时光牛马8 小时前
从零到实战,完整AI学习路线
前端
三十而立洋8 小时前
深入理解 Monorepo:子包安装依赖
前端·前端工程化
IT_陈寒8 小时前
Java Stream处理大集合,我的内存怎么就炸了
前端·人工智能·后端