前言
连续忙了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.xxxarduino// 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 架构的实战项目。