Uninote
Uninote
用户根目录
brdr
common
programming
docs
tests
后端试题
问题讨论

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 保留现有模块制和权限接口,发布渲染页的受控窗口函数继续提供给导出流程。
  • 完整构建清理旧的 .js runtime、日志和 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 依赖缓存命中不能省掉应用源码的这部分转换,因此仅等待依赖缓存预热不足以解决问题。

后续优化顺序

  1. 先减少开发启动必须等待的构建工作。
    • 评估将测试与非开发运行必需的目标移出启动关键路径。
    • 保证主进程、preload 和必要生成资源保持最新,同时保留独立完整类型检查。
    • 不能直接替换为 ensureBuild():该函数明确允许源码修改后继续使用旧构建。
  2. 复用同一次 HMR 转换中的转译和解析结果。
    • 保留语法错误检查、模块边界判断和安全刷新行为。
    • 修改后验证函数引用、撤销历史和未保存草稿恢复。
  3. 完成前两项后,再测量是否需要延迟加载更多 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。

references

settings-keyboard

点赞(0) 阅读(25) 举报
目录
标题