Vite renderer 开发
命令
npm ci
npm run dev
dev先检查开发构建缓存,再启动 Vite 8.3.4 和 Electron;输入与产物未变时不启动tsc。- 开发服务仅监听
127.0.0.1,使用自动分配的端口。 - 保存 renderer 源码后由 Vite 按需转译,不需要重新运行完整构建。
- Vite 保存时不执行完整类型检查;提交前仍需运行
npm run typecheck。 - 修改主进程、preload、开发工具配置或依赖后,退出应用并重新运行
npm run dev。 - 关闭 Electron 后自动关闭开发服务;终端中断通过父子进程 IPC 请求正常退出,使用既有恢复稿保存流程。
npm start继续复用静态构建,不启用开发服务器。npm run build:dev仍只执行增量构建。- 完整构建、生产打包和静态页面加载链路保留,Vite 仅作为开发依赖。
开发构建缓存
- 使用独立的
.build/.dev-build-state.json,记录输入内容摘要、配置和产物清单。 tsconfig.dev.json只以主进程、preload、开发 Electron 入口和生成资源工具作为代码入口。- 普通开发启动不编译测试目标,也不运行编辑器 Rollup 打包。
- Windows 回收站辅助脚本继续使用独立 WSH 目标,保持 ES5 和无 CommonJS 的输出。
- renderer
.mts由 Vite 提供,修改、增加或删除这类文件不会触发启动前的 TypeScript 编译。- 发布和导出页面也从 Vite 加载当前源码,避免依赖旧的静态 renderer 产物。
src/shared和主进程的 ESM 仍参与开发构建,保证 Electron 读取最新代码。
- 主进程、preload、共享代码、类型声明或开发脚本改变时,更新开发目标。
- WSH 源码单独判断;仅修改主进程不会重复编译 WSH。
- 改变目标后仍使用 TypeScript 增量编译,由编译器判断受影响文件,保留编译错误检查。
- HTML、CSS 等资源单独检查并复制,不因内容变化启动编译器。
- 编译配置、包清单、锁文件改变,记录中的产物缺失,或受跟踪输入删除时,清理并重建开发产物。
- 构建开始前撤销成功标记;失败后不会复用不完整产物。
- 开发与完整构建共享
.build,但不共享成功标记。- 切换到完整构建或静态启动时重新生成完整产物,反向切换时重新建立开发缓存。
- 首次启动和缓存失效后的启动仍需要编译;加速的是后续无变化或仅 renderer/资源变化的启动。
- 不要同时运行会重建
.build的开发、构建或测试命令。
npm run test:build覆盖开发缓存命中、选择性编译、资源更新、失败恢复、删除和缺失产物等场景。npm run test:dev为编译测试夹具仍执行完整增量构建;普通npm run dev使用上述快速路径。- 更新开发启动器后,退出现有 Electron 开发实例,并重新运行
npm run dev。
优化后验证(2026-10-13)
- 为避免与正在运行的开发实例争用
.build,在源码临时副本中验证,复用本机依赖和 Node 22.23.2。 - 构建回归、四项 HMR 单元测试、真实 Electron 开发冒烟及发布/导出 Mermaid 场景通过。
- 项目类型检查、修改源码的 ESLint 和
git diff --check通过。- 完整
npm run lint被仓库已有的七个超 800 行文件拦截;本次修改的源文件均未超限。
- 完整
- 使用与优化前相同的临时观察入口和独立用户目录测量。
- 开发缓存命中后,未启动任何编译或生成资源子进程,也未执行资源目录复制。
- 开发进程起点至 Electron spawn 为 0.774 秒;至
mynoteDevReady为 4.336 秒。 - 优化前缓存命中的对应总时间为 10.641–12.133 秒。
- 这是本机一次复测,不是独占机器的统计基准,不包含 npm 自身启动和真实笔记库恢复成本。
- 首次建立开发缓存仍需编译;该轮从开发进程起点至界面就绪约 10.5 秒。
ESM 模块结构
- 主窗口、片段窗口、Quick Cmd Form 和发布渲染页均通过模块脚本加载。
- Renderer 源码使用
.mts,静态构建输出.mjs。 - 源 HTML 引用
.mjs构建入口;开发服务将页面入口改为.mts源码。 renderer/dependencies.mts显式导出共用工具和 UI 对象,并在入口调用时挂载对话框及界面。renderer/runtime/state.mts为每个窗口提供一个有类型的共享状态对象。- 只保存跨模块使用的状态,各模块通过对象属性读写同一份状态,不复制可变值。
- 单个模块独占的控件引用、计时器、缓存和交互标记使用模块私有变量。
- 组件局部初始化由所属模块负责,在注册事件或使用控件前完成。
- 状态字段由初始化函数按原有顺序填充;模块求值期间不读取尚未初始化的字段。
renderer.mts显式导入并按依赖顺序调用业务模块的初始化函数;当前入口及覆盖检查分别见src/renderer.mts和test/module-extraction.test.ts。- 函数和共享类型通过命名导入连接,不再依赖全局词法变量或动态 classic script 加载。
- 事件注册和状态初始化只在启动时执行一次。
- 加载状态与异步笔记库恢复完成信号继续供自动化和开发刷新使用。
- 日志序列化由
shared/log-values.mts共用;已启用日志的页面入口先安装日志镜像,再初始化业务。 - 主进程与 preload 保留现有模块制和权限接口,发布渲染页的受控窗口函数继续提供给导出流程。
- 完整构建清理旧的
.jsruntime、日志和 Quick Cmd Form 产物。迁移后重新构建并重启 Electron;开发模式重新运行npm run dev。
热更新边界
原地更新,保留编辑器实例
- CSS 使用 Vite 热更新。
- 无状态 ESM 函数模块使用稳定的转发函数接受 HMR。
- 已被其他模块或共享运行时捕获的函数引用继续调用新的实现。
- 模块只允许函数、导入和无初始化求值的常量。
- 可变状态、对象或数组缓存、类、回调工厂及初始化副作用不进入此边界。
renderer/runtime/**/*.mts中的业务函数修改使用 ESM 稳定转发函数热替换。- 包含模块私有可变变量的模块不进入函数热替换边界,使用下述安全刷新流程。
- 热替换只执行函数实现赋值,不重复执行变量初始化和事件注册。
- 修改导入、函数签名、初始化函数、函数之外的代码,或增加和删除函数时改用安全刷新。
- Vite 在重新求值之前检查该边界,避免先替换部分实现再发现需要刷新。
- 原地更新保留未保存内容、标签、CodeMirror 实例、选区、滚动位置及撤销历史。
- 更新函数后,在下一次调用该函数时使用新实现;已经生成的静态 DOM 不会自动重绘。
安全刷新,恢复窗口内容
- 初始化代码、状态类、模块导出结构、回调工厂和 HTML 修改可能需要刷新。
- 主笔记窗口先等待初始化完成,再保存恢复稿和当前窗口的
sessionStorage快照。- 快照包含未命名笔记、标签、未保存内容、活动标签及选区/视图状态。
- 当前笔记库通过现有受控 API 恢复。
- 恢复成功后才删除快照。
- 打开的对话框关闭前推迟刷新,避免打断录音或丢弃对话框草稿。
- 恢复稿写入失败或快照超过存储配额时取消刷新,显示应用内提示并保留当前编辑器。
- 刷新会重建编辑器,因此不保留撤销历史或临时对话框状态。
- 代码片段和 Quick Cmd Form 页面也连接 Vite;焦点位于可编辑控件或对话框打开时推迟页面刷新。
- 这两个辅助窗口没有主笔记窗口的完整快照恢复能力。
Electron 加载与安全边界
- Electron 开发入口是
scripts/dev-electron.mts;生产主入口不变。 - 页面仍通过
file://加载,保持本地图片、音频、日志和麦克风权限的原有 URL 校验。 - 仅将开发页面 HTML 中的脚本和样式指向回环开发服务。
- CSP 仅增加当前开发服务的 HTTP/WebSocket 地址,没有启用
unsafe-eval或关闭webSecurity。 - 保留
contextIsolation: true、sandbox: true和nodeIntegration: false。 - 开发专用 preload 仅提供刷新请求;主进程验证发送者是已知开发页面的主帧后才刷新。
- 原有生产 preload 和 Markdown API 不增加开发权限。
.mjs导入在开发模式下解析到维护的.mts源码;业务初始化仍按原有顺序执行。- 开发模式将编辑器 bundle 的导入解析到维护的 CodeMirror 源码入口,适配器修改也参与安全刷新。
ignore浏览器包装复用.build产物;生产编辑器打包和许可证生成链路保留。- Mermaid 保留运行时 URL 解析,避免 Vite 将依赖地址改写为源码目录之外的路径。
- 开发模式通过回环服务加载依赖,静态页面继续按原来的包根目录路径加载。
启动耗时排查(2026-10-13)
测量范围
- 环境:macOS 15.3.1、x86_64、Node.js 22.23.2,代码基线
6378d55。 - 保留工作区现有的
src/index.html文案修改;未更改应用或构建源码。 - 使用临时 Node preload 观察编译子进程、资源复制和 Electron 启动时间。
- 第一轮直接运行
scripts/dev.mts,后两轮通过npm run dev运行同一入口。 - 计时从开发进程 preload 开始,不含 npm 自身启动时间。
- Electron 观察入口导入真实
dev-electron.mjs,每轮使用独立的临时用户数据目录。 - 禁用测量实例的全局快捷键注册,保留原有开发实例运行。
- 以
mynoteDevReady为终点,包含 renderer 初始化和开发恢复钩子安装。 - 每 50 ms 检查一次就绪状态;统计存在轮询和测量开销。
- 第一轮直接运行
- 三轮均已有 TypeScript 增量缓存;第一轮触发 Vite 依赖重新优化。
- 后两轮命中 Vite 磁盘依赖缓存,但新服务仍需重新转换应用模块。
- 第二轮另使用 Node inspector CPU 采样,第三轮关闭采样复测。
- 这些数据不包含真实笔记库、标签恢复、同步和大目录扫描成本。
- 未删除
.build测量全新构建,也未中断已有开发实例;结果不是独占机器的冷启动基准。
实测结果
单位为秒;各行阶段耗时不可与累计时间重复相加。
| 阶段 | 首轮/依赖重新优化 | 缓存命中/CPU 采样 | 缓存命中/无采样 |
|---|---|---|---|
| Node 目标增量编译 | 3.678 | 3.699 | 3.143 |
| Renderer 目标增量编译 | 1.207 | 1.324 | 1.329 |
| Scripts 目标增量编译 | 1.387 | 1.402 | 1.309 |
| WSH 目标增量编译 | 0.233 | 0.254 | 0.231 |
| 四次编译合计 | 6.505 | 6.679 | 6.012 |
src 与 test 资源复制 |
0.057 | 0.055 | 0.047 |
| 开发进程开始至 Electron spawn(累计) | 7.526 | 7.683 | 6.842 |
| Electron spawn 至窗口创建 | 0.887 | 0.938 | 0.850 |
| Electron spawn 至 renderer 就绪 | 4.558 | 4.450 | 3.799 |
| 开发进程开始至 renderer 就绪(累计) | 12.084 | 12.133 | 10.641 |
- 缓存命中时,启动前编译仍占端到端时间约 55%–57%。
- 资源复制完成至输出开发服务地址仅约 0.09–0.11 秒;该区间还包含构建收尾工作。
- 页面记录到 159 个资源条目;导航开始至
load为约 2.83–3.41 秒。 - 三轮均未执行编辑器 Rollup 打包,因此本次耗时不能归因于编辑器重复打包。
- 首轮临时数据路径使用
/tmp,Chromium 输出缓存目录警告。- 后两轮改用
os.tmpdir()后没有该警告,启动仍需约 10.6–12.1 秒。
- 后两轮改用
已定位的耗时来源
启动前串行编译
scripts/dev.mts必须等待build(true)完成后才启动 Vite 和 Electron。scripts/build.mts每次启动都串行执行四个tsc,即使源码没有变化。- 增量缓存避免了大部分检查和输出,但每个新编译进程仍需读取、解析并绑定项目。
- 对 Node 目标单独运行
--extendedDiagnostics,确认缓存命中后仍加载 1,139 个文件。- TypeScript 内部统计:Program 1.98 秒,Bind 0.61 秒,Total 2.59 秒。
- Program 包含解析和模块解析,不应再与 Parse 1.22 秒重复相加。
- 内部 Total 不包含完整的 Node 进程启动成本,不能直接当作上述子进程墙钟耗时。
tsconfig.node.json覆盖主进程、服务器、开发脚本和测试,开发启动也承担了测试相关输入成本。
无修改时仍然耗时的原因
- 追加实验连续执行两轮四目标编译,沿用原有参数和缓存,并增加:
--extendedDiagnostics:区分读取、解析、绑定和类型检查。--listEmittedFiles:检查是否实际重新输出文件。
- 八次命令全部成功,没有
TSFILE输出,也没有报告 Check 或 Emit 阶段耗时。- 四份
.tsbuildinfo每次执行前后的 SHA-256 和修改时间均一致。 Instantiations均为 0;本次慢启动没有表现出复杂泛型重复检查的特征。- 这些证据说明缓存确实命中,耗时主要是重建编译进程内的项目表示。
- 四份
第二轮文件缓存较热时的结果如下,单位为秒:
| 目标 | 加载文件数 | Program | 其中 Parse | Bind | 编译器 Total | 子进程墙钟 |
|---|---|---|---|---|---|---|
| Node | 1,139 | 1.69 | 1.16 | 0.62 | 2.31 | 2.675 |
| Renderer | 485 | 0.71 | 0.48 | 0.26 | 0.97 | 1.206 |
| Scripts | 516 | 0.77 | 0.52 | 0.31 | 1.08 | 1.300 |
| WSH | 4 | 0.06 | 0.05 | 0.03 | 0.08 | 0.234 |
- Program 包含读取、解析和依赖图构建;Parse 是其中一部分,不能重复相加。
- Bind 是建立作用域和符号关系,仍需处理本轮重新创建的语法树。
- 四个子进程墙钟合计 5.415 秒;编译器 Total 合计 4.44 秒。
- 差额包含 Node/编译器加载、配置处理及统计区间外的收尾等开销,不等同于纯进程创建时间。
- 第一轮四个子进程合计 6.633 秒;Node 目标 I/O Read 从 0.69 秒降到第二轮的 0.11 秒。
- 文件系统缓存影响波动,但缓存较热后仍有约 5.4 秒固定工作。
本地 TypeScript 6.0.3 的 lib/_tsc.js 也能验证这一执行路径:
createIncrementalProgram读取 build info,再调用 builder 创建本轮程序。createBuilderProgramUsingIncrementalBuildInfo恢复文件版本、签名、依赖关系和诊断信息。- 它的
getProgramOrUndefined返回undefined,没有恢复上一次进程的完整语法树。
- 它的
setGetSourceFileAsHashVersioned调用原有源码加载函数,再从源码文本计算版本摘要。- 因此当前的
tsc -p --incremental仍需加载和解析输入,然后才能复用检查结果、跳过输出。- 磁盘增量缓存与常驻 watch 进程的内存复用不同。
- 官方 incremental 说明将其描述为保存项目图信息。
四目标划分进一步放大了这些固定工作:
- 四次共加载 2,144 个文件条目,去重后只有 1,210 个路径。
- Node、Renderer、Scripts 三个目标共同包含 463 个文件;独立进程不能共享它们的语法树。
- Node 目标包括 684 个依赖文件、167 个
test文件和 128 个src/renderer文件。 - Scripts 目标包括 51 个测试文件,但会沿导入加载 129 个
src/renderer文件。- 例如
test/smoke/renderer/context-menus.ts直接导入 runtime 的状态、编辑器和树模块。
- 例如
types/renderer-globals.d.ts还通过类型导入引用 CodeMirror 适配器和快捷键服务。include决定入口文件,导入关系仍会带入其他源码。skipLibCheck已开启,但不能省掉声明文件的读取、解析和符号绑定。
优先评估在创建编译程序之前跳过未变化的项目,其次缩小开发启动的项目依赖范围。
TypeScript 的 tsc -b 提供项目是否最新的检查,见官方 build 模式说明。
当前脚本仍调用四次 tsc -p,根配置的 references 没有让这四次调用自动变成 build 调度。
这只是优化方向:尚未验证本项目切换后的收益;需要统一 build-info 配置并验证共享输出与失效条件。
本次没有修改编译参数,也没有绕过类型检查。
页面加载时的 HMR 分析
scripts/renderer/vite.mts的转换钩子对.mts应用模块执行 HMR 分析。- runtime 模块先调用
runtimeSkeleton,再调用moduleHotFunctions。- 两者都调用
javascript,内部执行ts.transpileModule。 - 两者还分别创建语法树;同一个 runtime 输入在首次加载时发生重复转译和解析。
- 两者都调用
- 第二轮 CPU 采样中,调用栈归属 HMR 分析的样本约 2.23 秒。
- 其中
javascript及其子调用约 1.81 秒。 - 样本按最近的 HMR 函数归属,避免父子调用重复计数。
- 采样只覆盖开发服务进程,不覆盖 Electron;不是修改后可直接节省时间的保证。
- 其中
- Vite 依赖缓存命中不能省掉应用源码的这部分转换,因此仅等待依赖缓存预热不足以解决问题。
后续优化顺序
- 先减少开发启动必须等待的构建工作。
- 评估将测试与非开发运行必需的目标移出启动关键路径。
- 保证主进程、preload 和必要生成资源保持最新,同时保留独立完整类型检查。
- 不能直接替换为
ensureBuild():该函数明确允许源码修改后继续使用旧构建。
- 复用同一次 HMR 转换中的转译和解析结果。
- 保留语法错误检查、模块边界判断和安全刷新行为。
- 修改后验证函数引用、撤销历史和未保存草稿恢复。
- 完成前两项后,再测量是否需要延迟加载更多 renderer 功能模块。
- 本次没有测量各业务模块初始化占比,不据此认定某个业务功能是瓶颈。
以上为优化前的排查基线;后续已实现启动缓存优化,当前行为见“开发构建缓存”。 HMR 重复转译优化尚未实施,未引入常驻启动监控。
复核方法
单独查看 Node 目标的增量编译内部耗时:
node node_modules/typescript/bin/tsc -p tsconfig.node.json \
--incremental --tsBuildInfoFile .build/.cache/tsconfig.node.json.tsbuildinfo \
--extendedDiagnostics
- 对比启动性能时,分别记录开发进程起点、四个编译子进程、Electron spawn 和
mynoteDevReady。 - 使用独立用户数据目录,区分依赖缓存命中和全新构建;不要把
test:dev的全部场景耗时当成启动耗时。 - 本次
npm run test:dev通过:ESM/runtime 热更新、CSS、编辑器实例、撤销历史、文件来源和草稿/选区恢复。- 日志中的
smoke recovery write failure是测试主动注入的失败,用于验证保存失败时取消刷新。
- 日志中的
- 文档变更通过
git diff --check。
验证
npm run typecheck
npm run lint
npm run test:dev
test/renderer-hmr.test.mts验证稳定函数引用、ESM 共享状态、初始化只执行一次、安全模块边界和开发 CSP。test/renderer-access.test.ts验证测试访问使用真实 ESM 状态,保留局部变量遮蔽和对象属性含义。- Electron 场景夹具显式导入 runtime 导出;内联断言由测试专用适配器连接相同模块,不向生产页面恢复旧全局变量。
test:dev使用独立用户数据目录和临时探针文件,运行真实 Electron 窗口。- 验证发布/导出页面使用 Vite 源码,并能加载 Mermaid 生成 SVG。
- 验证 ESM、共享运行时和 CSS 更新。
- 验证编辑器实例、选区、撤销历史保持不变。
- 验证恢复写入失败时取消刷新,以及安全刷新后恢复未保存内容和选区。
- 探针通过独占创建,结束时清理,不修改已有 renderer 文件。
npm test包含该开发模式冒烟测试。- 迁移前 Windows 基线:类型检查、lint、684 项单元测试、完整构建回归和 Vite Electron 冒烟测试通过。
- ESM 迁移的 Windows 验证:687 项单元测试、42 项 Electron 冒烟、Vite HMR、构建回归和目录包检查通过;详情见测试结果。
- 未验证 macOS/Linux 真机、真实麦克风设备和发布安装包。
- 参考:Vite HMR API、Electron protocol。
