背景
当前实际使用场景中,待办列表同时显示/管理的内容 少于 100 条 。因此本次性能治理不需要迁移到 QListView + QAbstractListModel + Delegate,应优先消除现有 QWidget 架构中的无效主线程工作。
当前代码存在以下共同根因:
master_timer 每秒执行 tick_update(),扫描全部任务后遍历全部卡片,并对每张卡片调用 update_timer_display()。
update_timer_display() 即使状态和显示文字没有变化,仍会重复设置完成图标、样式表、字体并调用 update_text_display() 重新测量布局。
每次 Tick 结束都会执行 _sync_todo_card_sizes() 和 QListWidget.doItemsLayout()。
创建、编辑、完成状态切换、删除和提醒处置都会保存完整 JSON,然后调用 update_list_widget() 清空并重建全部卡片。
单张卡片初始化过程中计时显示会被重复刷新;每次整表重建还会重新创建卡片控件、详情浮层、按钮、图标和信号连接。
TaskEditDialog 以主窗口为 parent,关闭后没有显式释放,需要确认反复打开后是否积累隐藏子对象和主题信号连接。
此前对当前存储实现的微基准显示,在同一测试环境中:
任务数
JSON 大小
加载中位数
保存中位数
10
4 KB
0.04 ms
1.64 ms
100
40 KB
0.34 ms
2.50 ms
绝对数值不代表 Windows/杀毒软件环境,但足以说明:百条以内场景的首要瓶颈是 UI 重建和无效布局,而不是 JSON 解析。原子写入、备份和损坏保护不能为了提速而削弱。
相关代码:
todo_app/main_window.py::tick_update
todo_app/main_window.py::update_list_widget
todo_app/main_window.py::_sync_todo_card_sizes
todo_app/widgets.py::TodoItemWidget.update_timer_display
todo_app/dialogs.py::TaskEditDialog
todo_app/storage.py::save_todos
目标
在不改变界面、提醒、筛选排序和存储安全语义的前提下,针对 0~100 条任务 :
降低空闲状态下每秒 Tick 的主线程开销;
消除滑动过程中周期性的样式和布局抖动;
缩短添加、编辑、完成、删除及提醒弹窗的响应时间;
减少整表控件销毁和重建;
建立可重复的 Windows 性能基线与回归门禁。
实施范围
1. 建立可观测的性能基线
在修改前后使用 0、10、50、100 条固定数据分别记录:
应用初始化到首个完整列表可见;
添加/编辑弹窗从点击到可交互;
删除确认后到列表稳定;
添加、编辑、完成切换、删除的处理时间;
tick_update() 的中位数与 P95;
连续滑动期间是否出现与 1 秒 Tick 对齐的主线程长帧;
反复打开并关闭编辑弹窗后的 TaskEditDialog 子对象数量。
要求:
至少在 Windows 开发环境和正式单文件 EXE 各记录一组结果;
性能日志默认关闭,只能由测试参数或环境变量显式启用;
CI 以调用次数和对象复用等确定性断言为主,不能仅依赖易抖动的毫秒阈值。
2. 拆分任务状态刷新与计时文字刷新
将“完成状态/图标/字体/卡片配色”和“计时文字”拆分为不同更新路径。
缓存上一轮渲染状态;状态、文字和颜色均未变化时直接返回。
普通 Tick 不得重复调用 setIcon、setStyleSheet、setFont 或重建操作图标。
保留对全部 self.todos 的提醒扫描,不能因为筛选或不可见而漏提醒。
可见卡片只在其最终显示文字变化时更新;屏幕外卡片不做无意义的 QWidget 刷新。
从无变化的每秒 Tick 中移除 _sync_todo_card_sizes();布局同步只在以下情况发生:
viewport 宽度变化;
任务正文、字体或主题变化;
卡片加入、移除或逻辑行数变化;
筛选/排序结果变化。
缓存稳定的 SVG/QIcon 和程序化操作图标,避免每秒或每次全量刷新重复构造。
3. 用差量协调替代无条件整表重建
保留筛选和排序结果的重新计算,但复用未变化的 QListWidgetItem 与 TodoItemWidget:
添加:只创建新任务对应卡片,并插入正确位置;
编辑/完成切换:更新目标卡片;若筛选归属或排序位置变化,仅移动/移除该卡片;
删除:只删除目标卡片;
提醒完成、推迟、忽略:只更新受影响任务;
仅在无法安全协调的异常恢复路径允许完整重建。
差量更新必须:
维护稳定的 task_id -> item/widget 映射;
避免重复连接信号;
保持当前筛选、排序和空列表占位语义;
尽量保留滚动位置及当前可见区域;
继续在用户操作后调用现有 save_todos(),不修改原子保存、备份和损坏保护规则。
4. 收敛启动与弹窗生命周期
单张卡片在初次插入时只执行一次完整状态初始化,避免构造、应用主题、插入列表后重复刷新。
启动阶段先记录各阶段耗时,再决定是否延迟非必要对象;不得为了“视觉上先显示”引入明显空白闪烁。
检查 TaskEditDialog 在 exec() 返回后的释放方式,确保重复添加/编辑不会持续增加主窗口子对象或残留主题信号。
通知弹窗仍需先写入提醒去重状态;优化后应尽快显示,不再被无关卡片的整表重建阻塞。
提醒窗口的逐项完成、推迟、忽略及唯一非模态窗口语义保持不变。
验收标准
100 条任务、数据和界面均无变化时:
每秒 Tick 不调用 _sync_todo_card_sizes() 或 QListWidget.doItemsLayout();
不重复设置卡片图标、静态样式或字体;
不调用 update_list_widget();
tick_update() P95 相比修改前下降至少 80%。
添加、编辑、完成切换和删除一个任务时:
不调用 QListWidget.clear();
未变化任务的 TodoItemWidget 对象身份保持不变;
仅目标任务及必要的排序/筛选位置发生变化;
同机 P95 相比修改前下降至少 50%。
100 条任务连续滑动 60 秒,不出现与每秒 Tick 同步的明显卡顿;主线程不得出现由全列表布局导致的周期性长帧。
初始构建中,每张卡片只进行一次完整状态/计时初始化;首个完整列表可见时间不得回退超过 10%,并记录实际优化结果。
连续打开并关闭添加/编辑弹窗 100 次后,不持续累积已关闭的 TaskEditDialog、内部控件或主题信号接收者。
筛选、排序、提醒扫描、完成、推迟、忽略、托盘恢复、详情浮层和深浅主题行为保持一致。
save_todos() 的原子替换、单份备份、损坏文件只读恢复和拒绝覆盖语义零变化。
增加针对无变化 Tick、差量 CRUD、对象复用、滚动位置和弹窗释放的自动化测试。
完整执行:
python -m compileall todo_app
python -m unittest discover -s tests -v
git diff --check
PR 中附修改前后 Windows 开发版与单文件 EXE 的性能表,并说明无法稳定量化的剩余风险。
风险与防护
漏提醒 :只减少 QWidget 刷新,不减少对全部任务数据的提醒扫描。
筛选/排序错误 :字段变化后必须重新判断目标任务是否仍应显示以及应处位置。
控件状态陈旧 :状态刷新和计时刷新拆分后,要覆盖完成、取消完成、推迟、到期、忽略和主题切换。
信号重复 :复用卡片时禁止重复 connect;删除卡片时同步断开和释放相关浮层。
数据可靠性回退 :不通过延迟或异步保存削弱现有崩溃安全保证。
与现有 Issue 的关系
非目标
不迁移到 QAbstractListModel、Delegate 或其他列表虚拟化架构。
不以数千条任务作为设计容量。
不修改 JSON 字段、存储位置或文件格式。
不把同步持久化改成可能丢失最后一次操作的后台延迟保存。
不修改 UI 视觉、提醒语义、版本规则或正式发布流程。
不处理 pip、PyInstaller 缓存和 Actions 构建耗时,相关工作继续由 优化 Windows 自动打包耗时并保留正式发布的干净构建 #79 跟踪。
背景
当前实际使用场景中,待办列表同时显示/管理的内容 少于 100 条。因此本次性能治理不需要迁移到
QListView + QAbstractListModel + Delegate,应优先消除现有 QWidget 架构中的无效主线程工作。当前代码存在以下共同根因:
master_timer每秒执行tick_update(),扫描全部任务后遍历全部卡片,并对每张卡片调用update_timer_display()。update_timer_display()即使状态和显示文字没有变化,仍会重复设置完成图标、样式表、字体并调用update_text_display()重新测量布局。_sync_todo_card_sizes()和QListWidget.doItemsLayout()。update_list_widget()清空并重建全部卡片。TaskEditDialog以主窗口为 parent,关闭后没有显式释放,需要确认反复打开后是否积累隐藏子对象和主题信号连接。此前对当前存储实现的微基准显示,在同一测试环境中:
绝对数值不代表 Windows/杀毒软件环境,但足以说明:百条以内场景的首要瓶颈是 UI 重建和无效布局,而不是 JSON 解析。原子写入、备份和损坏保护不能为了提速而削弱。
相关代码:
todo_app/main_window.py::tick_updatetodo_app/main_window.py::update_list_widgettodo_app/main_window.py::_sync_todo_card_sizestodo_app/widgets.py::TodoItemWidget.update_timer_displaytodo_app/dialogs.py::TaskEditDialogtodo_app/storage.py::save_todos目标
在不改变界面、提醒、筛选排序和存储安全语义的前提下,针对 0~100 条任务:
实施范围
1. 建立可观测的性能基线
在修改前后使用 0、10、50、100 条固定数据分别记录:
tick_update()的中位数与 P95;TaskEditDialog子对象数量。要求:
2. 拆分任务状态刷新与计时文字刷新
setIcon、setStyleSheet、setFont或重建操作图标。self.todos的提醒扫描,不能因为筛选或不可见而漏提醒。_sync_todo_card_sizes();布局同步只在以下情况发生:3. 用差量协调替代无条件整表重建
保留筛选和排序结果的重新计算,但复用未变化的
QListWidgetItem与TodoItemWidget:差量更新必须:
task_id -> item/widget映射;save_todos(),不修改原子保存、备份和损坏保护规则。4. 收敛启动与弹窗生命周期
TaskEditDialog在exec()返回后的释放方式,确保重复添加/编辑不会持续增加主窗口子对象或残留主题信号。验收标准
_sync_todo_card_sizes()或QListWidget.doItemsLayout();update_list_widget();tick_update()P95 相比修改前下降至少 80%。QListWidget.clear();TodoItemWidget对象身份保持不变;TaskEditDialog、内部控件或主题信号接收者。save_todos()的原子替换、单份备份、损坏文件只读恢复和拒绝覆盖语义零变化。python -m compileall todo_apppython -m unittest discover -s tests -vgit diff --check风险与防护
与现有 Issue 的关系
非目标
QAbstractListModel、Delegate 或其他列表虚拟化架构。