19 KiB
启动拆包与工具栏状态经验总结
更新日期:2026-04-21
这轮最值得记住的规律
1. 启动进度只能有一个“百分比真源”
这轮 loading 卡顿感的根因,不是进度条本身,而是进度来源混杂:
- HTML 启动壳自己在写阶段百分比
- 主线程初始化阶段也在写百分比
- Service Worker 又在回传资源进度
结果就是进度条会“跳”“卡”“倒像是更慢了”。
更稳的做法是:
SW未就绪前,只做短时间平滑模拟,让用户知道系统在工作SW一旦开始回传进度,就由SW接管后半段百分比- 其余阶段只更新文案,不再写数值
一句话:文案可以多源,百分比只能单源。
2. 懒加载要区分“触发器”和“内容体”
这轮多个两击问题,本质都和这个边界有关:
- 可以延后的是抽屉内容、弹窗内容、工具窗口内容
- 不该延后的是用户第一下要点到的按钮、触发器、壳层容器
如果把触发器本身也放进懒加载链路,第一次点击就可能只完成“挂载按钮/挂载容器”,第二次点击才真正打开内容,用户会直接感知成 bug。
推荐原则:
- 按钮、抽屉骨架、输入框外壳常驻
- 重内容、重依赖、低频模块按需挂载
- 首次点击允许局部 loading,但不允许“点击无响应”
3. 依赖编辑器上下文的组件,不能被随意挪出树外
这轮 AI 图片生成 报 useBoard must be used inside <Plait>,本质是把依赖 Plait 上下文的对话框,移到了错误的挂载层级。
经验是:
- 凡是依赖
useBoard()、useDrawnix()、选择态、画布上下文的组件 - 在做延迟加载、拆层、抽离壳层时
- 先确认它依赖的 Provider / Board Context 还在不在上游
尤其是:
- 对话框懒加载
- 抽屉懒加载
- “核心画布壳层 / 延后功能层”拆分
如果组件依赖的是“当前画布实例”,那它的懒加载位置必须仍在画布上下文树内。
4. setState(updater) 里不要顺手改别的面板状态
左侧“项目 / 工具箱 / 任务面板”要点两下的问题,根因不是点击事件丢了,而是 toggle 逻辑写成了:
- 在
setState(prev => ...)的 updater 里 - 顺手调用
closeAllDrawers() - 于是一次点击里混进了多份状态更新
这种写法在互斥面板场景里很容易让首击状态被冲掉。
更稳的方式是显式顺序化:
- 先判断目标当前是否已打开
- 如果要打开,先关闭其它互斥面板
- 最后再打开目标面板
一句话:updater 里只算当前值,不做跨状态副作用。
5. 左侧常驻工具栏的顺序,不能跟“最近激活顺序”绑定
这轮另一类体验问题是:左侧常驻工具点一下就换位。
根因不是图标组件,也不是 selected 高亮,而是工具栏数据源按 activationOrder 排序:
- 工具一打开 / 一恢复,激活顺序就刷新
- 工具栏再按这个顺序重排
- 用户看到的就是“按钮自己跑了”
对常驻工具栏来说,用户心智更接近“固定栏位”,不是“最近使用列表”。
所以更合理的规则是:
- pinned 工具始终保持既有栏位顺序
- 打开后只替换本栏位的状态
- 非 pinned 的临时实例再单独排到后面
一句话:常驻栏看重空间记忆,不看重最近激活。
6. 版本更新后的懒加载 chunk 失效,要有“一次性自恢复”
这次 DrawnixDeferredRuntime 的报错:
Failed to fetch dynamically imported module
本质不是业务逻辑错误,而是用户页面停留在旧版本,部署后旧 HTML / 旧运行时还在引用已经失效的 hash chunk。
这类问题的特点是:
- 用户刷新前一切正常
- 一旦触发某个
React.lazy()模块才爆 - 报错看起来像功能坏了,实际是版本切换窗口里的静态资源失配
更稳的处理方式不是把所有懒加载都改成手写 loader,而是补一层轻量兜底:
- 在
ErrorBoundary里识别动态导入 / chunk 失效错误 - 按“当前版本 + 当前模块”做一次性重试标记,避免死循环
- 自动刷新一次,并带时间戳参数绕过旧缓存
- 应用成功启动后再清理恢复参数,避免 URL 污染
一句话:发布后 chunk 失效不是“要不要刷新”的问题,而是“要不要帮用户只刷新一次并避免循环”的问题。
这轮适合沉淀成默认规则的做法
- 启动进度:前半段可模拟,后半段由资源进度单源接管
- 懒加载:只懒内容,不懒触发器
- 组件拆层:先确认上下文依赖,再移动挂载位置
- 互斥面板:显式开关,不在 updater 内联副作用
- 常驻工具栏:固定顺序优先于最近激活
- 版本切换:懒加载 chunk 失效时自动恢复一次,但必须有防循环保护
后续开发检查清单
- 这个 loading 百分比现在到底是谁在写?
- 这个入口第一次点击,是打开内容,还是只完成挂载?
- 这个懒加载组件是否依赖画布 / Provider 上下文?
- 如果新版本刚发布,这个懒加载失败是不是“旧页面引用失效 chunk”而不是业务异常?
- 这个 toggle 是否在 updater 里偷偷改了别的 state?
- 这个工具栏是“固定栏位”还是“最近使用列表”?排序规则是否和用户心智一致?
一句话总结
这轮真正值钱的经验不是“把 bug 修掉了”,而是明确了 6 条边界:
- 百分比单源
- 触发器常驻
- 上下文不丢
- toggle 不串副作用
- 常驻栏位不乱跳
- 失效 chunk 只自恢复一次
2026-05-17:首屏壳层拆分与 chunk 守门经验
现象
- 首屏慢不只来自代码体积,也可能来自运行时静态环和 chunk 归属错误。
- 这轮真正卡住构建的,不是业务组件本身,而是
ai-chat和tool-windows之间被常量文件、服务入口、懒加载边一起拉成了静态回边。 - 纯工具函数如果和会反向依赖
data/image的运行时模块放在同一个文件里,循环会在源码层先炸,然后再在 chunk 层放大。
架构变更
apps/web/src/main.tsx只保留最早期恢复与 bootstrap 入口,后续应用初始化下沉到bootstrap.tsx。Drawnix拆成首屏壳层和延后功能层,任务恢复、自动插入、字体/视频恢复等副作用交给 idle 启动器。- 动态导入失败恢复独立到
lazy-asset-recovery.ts,避免首屏入口顺手背上重恢复逻辑。 - 图片自然尺寸这类轻量能力单独下沉到纯工具模块,
utils/image.ts只保留导出和编辑逻辑。 tool-ids.ts这类纯常量不能被目录级tool-windows规则误收编,否则很容易把轻量调用侧拖回重 chunk。- 构建后校验不再只看入口 HTML,还要看入口静态依赖链、chunk SCC 和首屏 500KB 预算。
经验
- 先修运行时循环,再谈 chunk 瘦身。
- manualChunks 是分包守门,不是架构补丁。
- 纯常量、纯工具、纯类型文件尽量留在低层共享入口,别让它们进入重运行时桶。
- 首屏建议目标 200-300KB 未压缩,500KB 作为硬上限。
- 构建校验脚本最好同时守三件事:入口链、chunk 环、首屏体积。
- 当环只剩纯常量误入重 chunk 时,先给低层入口加分包例外,再考虑继续拆模块。
复盘清单
- 这个文件是不是既做纯工具,又回引了运行时服务?
- 这个常量是不是被错误分到重 chunk 里了?
- 这个懒加载边界是不是只该延后内容,不该拖延触发器?
- 构建守门脚本有没有把“能跑”升级成“能跑且不超预算”?
后面继续做启动性能和工具系统时,只要这 6 条不破,很多体验问题都会提前消失。
2026-04-22:SW 空闲预取与常驻工具条恢复经验
现象
- 左侧常驻工具 icon 只有在点开工具抽屉后才出现
SW日志显示已经激活,但页面侧SWChannelClient多次初始化超时- 开发态
idle-prefetch-manifest.json缺失,prefetchIdleGroups直接中止
根因
- 页面把“有
controller”误当成“有可用Service Worker”,但实际存在ready.active已激活、页面仍未被 controller 接管的窗口期 - 常驻工具条显示链路和完整
tool-window runtime启用链路绑死,导致“显示 icon”也被重运行时阻塞 - UI 又额外依赖
tool-windows分组预取完成信号,开发态 manifest 缺失时就会一直卡住 idle prefetch单次跑批如果人为截断,完成状态就会长期失真,最后只能靠超时兜底
最终方案
SWChannelClient初始化放宽为:controller或navigator.serviceWorker.ready.active任一可用即可建通道- 常驻工具条拆成两段:
icon 显示单独启用;点击 icon 时再按需启完整
tool-window runtime toolWindowService恢复 launcher 状态时优先读取完整工具定义,保证首屏图标和标题完整SW在activate后自动消费 idle prefetch 默认分组,并把完成状态广播给页面- 页面监听
SW_IDLE_PREFETCH_STATUS,但只把它当优化信号;超时后直接放行常驻工具条,避免功能被优化链路反向阻塞 idle prefetch仍用小并发,但不能再裁剪总任务数,否则完成分组永远不闭环
这次最值得固化的规则
Service Worker可用性判断要区分:controller是“当前页已受控”,ready.active是“后台已有可用 worker”,两者不是一回事- 首屏可见能力和重运行时能力要解耦: 先让用户看到、点得到,再做按需增强
- 预取状态只能作为加速信号,不能作为功能显示前置条件
- 开发态缺 manifest、生产态资源回源失败,都要保证 UI 有可退化路径
排障顺序建议
- 先看
navigator.serviceWorker.controller和navigator.serviceWorker.ready.active是否分叉 - 再看显示链路是否被 runtime 初始化、manifest、预取完成状态串联阻塞
- 最后才看图标数据源、launcher 恢复、工具定义是否缺字段
2026-04-26:SW 关闭场景下的通道初始化超时经验
现象
- 控制台持续出现
[SWChannelClient] Attempt 4 failed: Error: SW activation timeout - 调试状态里
supported: true,但controller: null、registration: null - 本地开发默认关闭 SW 或 URL 带
?sw=0时仍然触发了swChannelClient.initialize()
根因
- 页面只用“浏览器支持
serviceWorker”判断是否启动 SW 诊断链路,没有复用真正的 SW 启用开关 sw-console-capture为了尽早捕获日志,会主动初始化swChannelClient,结果在 SW 未注册时反复等待不存在的controllerchange- SW 通道自身没有识别
?sw=0,其他调用点即使绕过主入口,也可能重新进入初始化重试 - 日志队列在通道不可用时只进不出,虽然单条较小,但长期打开页面会形成不必要内存占用
固化规则
- SW 相关入口必须共享同一个开关语义:
生产和本地调试默认启用;
?sw=0显式关闭;本地默认启动命令必须同步 watch 最新sw.js - 调试、崩溃、控制台捕获这类旁路能力,不能因为“想诊断 SW”而绕过 SW 开关主动拉起通道
SWChannelClient.initialize()自身也要兜底识别?sw=0,避免未来新增调用点造成重复超时- 离线日志队列必须有容量上限,通道不可用时丢弃最旧项,不能无限积压
- 控制台捕获要过滤
[SWChannelClient]、[ServiceWorkerChannel]这类自身通道日志,避免错误日志再次驱动初始化与上报
排障顺序建议
- 先看当前 URL 是否带
?sw=0,以及本地是否正在 watch 最新sw.js - 再看是否存在
registration;如果没有注册,不要继续等controllerchange - 最后排查是谁调用了
swChannelClient.initialize(),优先检查控制台捕获、崩溃上报、调试面板等旁路链路
2026-04-27:左侧浮岛工具栏与抽屉联动经验
现象
- 用户浏览器或系统存在左侧边缘标签时,固定贴边工具栏容易误触冲突。
- 直接把工具栏整体外移虽然能避让,但看起来像布局 bug,破坏“贴边悬浮岛”的原始观感。
- 将工具栏改成可拖动后,拖动过程中任务队列抽屉会被关闭态动画带出来。
- 任务队列抽屉从屏幕边缘大幅滑入,动画过重;抽屉打开后,左侧画布底色和抽屉底色割裂。
- 顶部拖动把手、底部固定按钮区如果只改父容器圆角,局部背景仍会把圆角视觉盖平。
根因
- “避让侧边标签”是用户个体环境问题,不适合用全局默认外移解决。
- 工具栏位置、抽屉位置、提示浮层位置、画布 fit-frame 避让如果各自写死宽度,拖动后必然错位。
- 关闭态抽屉的
transform依赖工具栏右边界变量时,拖动工具栏会触发关闭态抽屉过渡,形成“被拖出来”的错觉。 - 任务队列抽屉和工具箱共用基础抽屉,但任务队列内容更重,应该用更轻的专属 motion。
- 抽屉只从工具栏右侧开始绘制时,工具栏左侧仍露出画布底色;抽屉色和画布色差会被用户感知成断层。
最终方案
- 默认仍贴屏幕左边缘,保持悬浮岛观感;只提供顶部拖动把手,让需要避让的用户手动拖入内侧。
- 工具栏横向位置用
localStorage轻量持久化,双击把手复位到默认贴边位置。 - 统一维护
--aitu-toolbar-left和--aitu-toolbar-right-edge,抽屉、提示、fit-frame 都读同一套右边界。 - 拖动过程中只更新 CSS 变量和 DOM style,拖动结束后再落 React state,减少高频重渲染。
- 关闭态抽屉从
translateX(-100%)隐藏,打开态再平移到工具栏右边界;拖动期间给根节点加aitu-toolbar-dragging禁用抽屉 transition。 - 任务队列抽屉使用轻量淡入:
160ms transform + 120ms opacity,关闭态保持visibility: hidden和pointer-events: none。 - 抽屉打开时用同色
box-shadow向左铺到屏幕边缘,工具栏 z-index 高于抽屉,让工具栏仍像浮在抽屉上。 - 工具栏底部固定区也要继承底部圆角,避免底部按钮区背景把父容器圆角盖平。
固化规则
- 默认位置优先保留产品最自然的视觉形态;兼容特殊环境时优先给用户可调能力,不要把所有用户都推到次优默认。
- 可拖动 UI 的高频 pointer move 不要每帧依赖 React state;能用 CSS 变量同步的位置,优先走 CSS 变量。
- 所有依赖工具栏边界的组件必须共享同一来源,不能散落硬编码宽度。
- 隐藏态元素如果跟随一个高频变化变量,要么禁用 transition,要么让隐藏态不依赖这个变量。
- 抽屉“内容从哪里开始”和“背景铺到哪里”可以分离:内容对齐工具栏,背景可延展到屏幕边缘。
- tooltip 只负责展示提示,不应接收 pointer events 挡住工具按钮点击。
- 改圆角时要检查内部固定区域、底部区域、滚动区域背景是否覆盖父容器弧度。
回归检查清单
- 默认桌面视口:工具栏
left = 0,右边界 CSS 变量等于工具栏实际右边界。 - 拖动把手:拖动时任务抽屉不外露,松手后位置持久化,双击可复位。
- 任务队列:首次打开有轻量动画,关闭态不可见且不可点击。
- 工具箱 / 项目抽屉 / 任务队列:打开后左边缘贴齐工具栏右边界。
- 抽屉打开后:工具栏左侧到屏幕边缘为抽屉同色背景,工具栏仍在上层。
- 移动端:不显示拖动把手,默认收起形态不变。
2026-04-27:右侧停靠工具栏与双向抽屉经验
现象
- 工具栏只允许拖到左侧内缘时,右手用户或右侧工作流无法获得同等避让能力。
- 工具栏拖到右侧后,项目/工具箱/任务抽屉如果仍向右展开,会被屏幕边界截断。
- 对话抽屉、小地图、缩放控件如果只按“右屏幕边缘”定位,会被右侧工具栏遮挡。
- 对话抽屉收起态如果只平移,不做
visibility/pointer-events隐藏,右侧仍会露出面板边缘。 - 抽屉内容对齐工具栏后,抽屉背景没有铺满到屏幕边缘时,会出现画布色和抽屉色断层。
根因
- 工具栏位置从“左边界变量”升级为“双侧停靠语义”后,依赖它的浮层也必须知道当前停靠方向。
- 右侧停靠时,遮挡区域不是工具栏右边界,而是
viewportWidth - toolbarLeft。 - 抽屉宽度、抽屉展开方向、导航控件避让、fit-frame 可视区计算,不能各自用硬编码边距。
- 关闭态元素跟随工具栏位置时,仅靠
transform容易在边缘露出残影或接收交互。
最终方案
- 工具栏可拖到屏幕最右侧,超过屏幕中线后在根节点标记
aitu-toolbar-dock-right。 - 继续用轻量 CSS 变量同步布局,不新增配置实体:
--aitu-toolbar-left、--aitu-toolbar-right-edge、--aitu-toolbar-right-dock-width、--aitu-toolbar-right-avoidance、--aitu-toolbar-side-panel-max-width。 - 右侧停靠时,项目/工具箱/任务抽屉向左展开,关闭态从屏幕外隐藏,打开态左边缘贴齐工具栏左侧。
- 右侧停靠的抽屉用同色
box-shadow向屏幕边缘延展背景,内容对齐和背景铺满分离处理。 SideDrawer的 resize 方向按停靠侧切换;右侧停靠时拖左边缘调整宽度。ChatDrawer右侧停靠时让出工具栏宽度,打开态背景铺到屏幕右边缘,关闭态使用opacity + visibility + pointer-events彻底隐藏。ViewNavigation/ 小地图同时避让工具栏占位和打开的对话抽屉;fit-frame 按左右遮挡分别计算可视区。- 顶部拖拽把手默认态进一步弱化,只在 hover、focus、dragging 时增强。
固化规则
- 可拖动悬浮工具栏一旦支持跨屏停靠,所有依赖边界的组件都要读统一 CSS 变量,不再假设工具栏永远在左侧。
- 判断抽屉展开方向应基于工具栏中心点所在半屏,而不是用户最后一次拖动方向。
- 右侧停靠时,导航控件的避让值要叠加工具栏宽度和已打开抽屉宽度,避免小地图被抽屉压住。
- 关闭态抽屉必须同时处理视觉隐藏和交互隐藏:
visibility: hidden、pointer-events: none、必要时配合opacity。 - 抽屉背景延展可以用同色阴影铺屏,但不要让内容区域跟着铺满,否则会破坏对齐和可读性。
- 改动右侧面板时,要同步回归 ChatDrawer、ViewNavigation、Minimap、fit-frame 和各类 toolbar-right 抽屉。
回归检查清单
- 工具栏拖到右边缘:工具栏不出屏,刷新后位置保留。
- 右侧工具栏打开项目/工具箱/任务抽屉:抽屉向左展开,不被屏幕裁切,背景铺到右侧屏幕边缘。
- 对话抽屉关闭态:面板不可见且不可点击,只保留把手。
- 对话抽屉打开态:右侧让出工具栏宽度,背景延伸到屏幕右侧。
- 对话抽屉打开 + 小地图展开:小地图和缩放控件避开“工具栏 + 对话抽屉”组合宽度。
- fit-frame:工具栏/抽屉在左侧或右侧时,Frame 都居中到剩余可视区。