前言
连续忙了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 过程分析
- 重构 formatter 后已使用
- 进行了数次试验后确诊: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
-
在
scripts/
目录新建generate-worker.js
,使用 CommonJS + esbuild 脚本打包 formatter.worker.ts- 用
esbuild.build({ ... })
指定 bundle / minify / esm - 输出为
public/workers/formatter.js
- 用
-
Worker 中给格式化类型动态 import
typescriptconst 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; // ... } }
-
修改 useFormatter.ts ,直接指向 public Worker js 文件
goconst worker = new Worker('/workers/formatter.js', { type: 'module' });
-
封装 shim :解决依赖库内部访问
process.xxx
arduino// process-shim.js export const process = { env: {}, argv: [], version: '' };
并在 esbuild 打包时
inject: [path.resolve(__dirname, './process-shim.js')]
-
alias 解决 DOM-only 版本库问题
-
如
decode-named-character-reference
-
不能用
require.resolve()
,会报 exports 错误 -
直接:
csharpalias: { '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-formatter
、js-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
架构的实战项目。