局域网答题 · 前端性能评估与缺陷检测报告¶
- 评估日期:2026-09-15
- 评估范围:学生端 / 教师端 / 管理端全部前端页面(HTML + 静态 JS/CSS + 懒加载引擎)
- 后端:FastAPI + SQLite(本地
quiz.db),静态资源经CachedStaticFiles托管(gzip +max-age=86400) - 测试环境:Windows / Node v24(网络层采集),局域网回环
127.0.0.1:8000
1. 测试方案¶
1.1 目标¶
- 评估页面加载速度、资源加载效率、交互响应时间。
- 定位前端性能瓶颈(资源加载、冗余代码、内存泄漏、重排等)。
- 检测前端功能缺陷(渲染异常、交互逻辑错误、兼容性)。
1.2 被测页面¶
首页(学生实时)/、整卷测验 /exam、管理后台场控 /admin、题库&组卷 /qbank、账号管理 /admin/users、统计报告 /admin/stats、房间管理 /admin/rooms、学校管理 /admin/schools、我的记录 /me、登录 /login、个人资料 /profile。
1.3 方法论¶
- 网络层真实测量:本地起服务,用
node:http(关闭自动解压)逐页请求 HTML 及所引用的所有/static/*资源,记录 TTFB、原始体积、gzip 后实际传输体积、请求数、缓存头。此数据为本报告核心实测值。 - 静态代码分析:通读全部 15 个前端 JS、4 个 CSS、12 个 HTML 模板,检查资源加载策略、轮询/定时器、事件监听生命周期、大列表渲染、懒加载等。
- Web Vitals(FCP/LCP/CLS/TTI):需在无头浏览器中运行。首轮评估时环境未安装 Playwright,故先以网络层实测 + 代码路径分析给出估算值(已明确标注);第三轮已装好 Playwright(Python 1.62 + 系统 Edge,无需下载 Chromium)并落地端到端点击流回归,见第 7 节。
1.4 指标定义¶
- TTFB:首字节时间;rawKB:未压缩总传输量;gzKB:gzip 后实际传输量;gzPct:gzip 节省比例;cacheOK:带
max-age的静态资源数。
2. 性能测试数据(真实采集)¶
| 页面 | HTTP | 请求数 | 原始KB | gzip后KB | 节省% | HTML原始KB | TTFB(ms) | 缓存资源 |
|---|---|---|---|---|---|---|---|---|
| 首页(学生实时) / | 200 | 6 | 176.0 | 102.7 | 41.7% | 69.8 | 4.4 | 5/5 |
| 整卷测验 /exam | 200 | 7 | 137.8 | 46.2 | 66.5% | 3.8 | 0.9 | 6/6 |
| 我的记录 /me | 200 | 5 | 70.3 | 24.0 | 65.9% | 2.5 | 1.1 | 4/4 |
| 登录 /login | 200 | 2 | 42.7 | 15.8 | 62.9% | 6.1 | 19.8 | 1/1 |
| 个人资料 /profile | 200 | 2 | 39.7 | 12.8 | 67.7% | 3.1 | 0.7 | 1/1 |
| 管理端页面 /admin 等 | 302 | — | — | — | — | — | — | 未登录重定向至 /login |
管理端页面(/admin、/qbank、/admin/users、/admin/stats、/admin/rooms、/admin/schools)在未登录态返回 302,无法直接采集其完整载荷;按其
<script>引用与文件体积估算见下表。
管理端页面载荷估算(未登录态为 302,按静态资源组成估算 gzip 后传输量)
| 页面 | 引用静态 JS(原始) | +CSS | 估算 gzip 后 | 页面 HTML 原始 |
|---|---|---|---|---|
| 题库&组卷 /qbank | ~154.6KB | +16.6 | ~50KB | 20.9KB |
| 场控 /admin | ~50.3KB | +36.5(style) | ~30KB | 84.0KB |
| 统计报告 /admin/stats | ~82.1KB | +36.5 | ~33KB | 8.0KB |
| 账号管理 /admin/users | ~92.0KB | +36.5 | ~35KB | 19.8KB |
| 房间管理 /admin/rooms | ~65.0KB | +36.5 | ~26KB | 10.3KB |
| 学校管理 /admin/schools | ~31.0KB | +36.5 | ~15KB | 5.4KB |
说明:wangEditor(1.3MB) 仅在打开题库编辑弹窗时懒加载;Excel 判分引擎(ExcelJS+XLSX+HyperFormula+JSZip ≈ 2.56MB) 仅在学生作答 Excel/WPS 题时懒加载,均不计入首屏。
2.1 关键结论(实测)¶
- TTFB 极佳:局域网回环下 0.5–33ms(首冷 33ms,后续 ≈1ms),服务端与网络不是瓶颈。
- 静态资源 gzip + 长期缓存已生效:所有
/static/*资源均带content-encoding: gzip且cache-control: max-age=86400,节省 41%–67% 传输量。二次访问静态资源命中缓存、几乎零传输。 - HTML 响应未压缩:所有页面 HTML 由
TemplateResponse返回,未走CachedStaticFiles的 gzip 分支(_GZIP_EXT仅含.js/.css/.json/.svg/.map/.txt),实测htmlGzKB == htmlRawKB。首页 HTML 69.8KB、场控 HTML 84.0KB 均以明文传输。 - 大内联脚本未独立缓存:首页
index.html含约 57KB 页面逻辑内联脚本、场控admin.html含约 84KB 内联脚本。内联脚本随 HTML 每次整页刷新重新下载,无法享受max-age=86400。
2.2 估算的渲染指标(需无头浏览器最终确认)¶
基于首屏传输量与渲染方式(原生 JS + innerHTML,无框架 hydration),估算:
- 首页 FCP/LCP:局域网内 < 300ms;公网(gzip 后 ~53KB)约 0.5–1.2s。
- 场控页:受 84KB 未压缩 HTML 拖累,公网下 LCP 可能达 1.5–2.5s(开启 HTML gzip 后显著下降)。
- 交互响应:选项点击/提交为同步 innerHTML + class 切换,单次 < 16ms,流畅;仅当列表一次性渲染上千项(见 P4)才会出现可感知卡顿。
3. 性能瓶颈与资源加载问题(按严重度)¶
P0 — HTML 响应未启用 gzip(影响所有页面首屏)¶
- 现象:所有 HTML 页面明文传输,首页 69.8KB、场控 84.0KB 未压缩。
- 位置:
main.pyCachedStaticFiles仅对静态扩展名压缩;HTML 由templates.TemplateResponse直接返回,未压缩。 - 影响:公网/弱网首屏传输量翻倍;场控页尤甚。
- 建议:为 HTML 响应增加 gzip(或在反向代理/EdgeOne 层开启
text/html压缩)。本地局域网可忽略,但公网部署收益大。
P1 — 大内联脚本未独立缓存(首页 / 场控重复下载)¶
- 现象:
index.html(~57KB 内联)、admin.html(~84KB 内联) 的页面逻辑写在 HTML 内,每次整页导航都重新下载且不可缓存。 - 位置:
templates/index.html第 125–1474 行内联脚本;templates/admin.html内联脚本。 - 影响:二次访问首页仍需下载 ~70KB(本可省);与 P0 叠加,场控页二次访问仍 ~84KB。
- 建议:将页面专属脚本抽成
/static/index.js、/static/admin.js(已带?v=版本号与max-age=86400),HTML 仅保留极薄引导。可即时把二次访问首页传输从 ~70KB 降到 ~0(静态已缓存)。
P2 — 实时答题轮询兜底频次偏高(WS 断线时)¶
- 现象:
index.html第 321 行setInterval(refreshNow, 800)在 WebSocket 未连时每 800ms 拉一次/api/current(≈1.25 req/s/人);第 324 行另有 10s 心跳。 - 位置:
templates/index.html321、324 行。 - 影响:正常(WS 连通)时仅 10s/次,开销极低;但 WS 不可用(如反代未透传 WS)时回退轮询会给服务端加压。
- 建议:可改用
uc.js已有的vizInterval(后台标签页暂停)并保留 800ms 兜底;同时确认部署链路 WS 透传。属健壮性优化,非首屏瓶颈。
P2 — 题库组卷拉取 limit=2000(大数据量校)¶
- 现象:
qbank.js第 884、913 行组卷时apiGet("/api/qbank/qs?parent=-1&limit=2000");第 153 行分页组件缺失时回退limit=2000一次性渲染最多 2000 张卡片。 - 位置:
static/qbank.js153、884、913 行。 - 影响:题库上千题的学校,组卷弹窗打开与渲染可能卡顿(一次性
innerHTML2000 行)。 - 建议:组卷选择改用服务端关键词搜索 + 分页/虚拟滚动,避免一次性拉全量;分页组件已默认启用,重点优化组卷拉取路径。
已验证的良好实践(正面,非问题)¶
- 静态资源 gzip +
max-age=86400缓存策略完整有效。 - Excel 判分引擎(~2.56MB)与 wangEditor(1.3MB)均懒加载,不进首屏。
uc.js的vizInterval(可见性感知轮询)已在场控/大厅/房间列表采用,后台标签页暂停请求。- 题库列表默认走
pager.js分页,非全量渲染。
4. 功能缺陷检测(Bug)¶
B1(已确认)— ucMount 重复绑定 document 级监听,存在监听器泄漏/重复执行¶
- 位置:
static/uc.js第 301–306 行(ucMount内document.addEventListener("click", ...)与document.addEventListener("keydown", ...))。 - 根因:
ucMount每次被调用都新增匿名document监听且不移除旧监听;index.html的renderNav()在一次登录流程中调用ucMount至少 2 次(afterLogin第 247、261 行),导致重复绑定。 - 影响:每次页面内
click/Esc会触发多次closeMenu;长会话/多次登录累积匿名监听,属典型内存泄漏模式。 - 复现:学生登录 → 浏览器控制台
getEventListeners(document)(DevTools)可见多个相同click监听。 - 建议:加幂等守卫(参考同文件 fetch hook 的
window.__quizRoomFetchHooked做法),或在ucMount开头removeEventListener旧 handler(改用命名函数)。
B2(已知风险,待浏览器回归)— 高复杂度交互路径未见自动化测试¶
- 材料大题逐小题提交/判分(
index.htmlmatSubmit/renderMaterial)、实时 Excel/WPS 判分重建(renderExcelLive换题重建面板)、抢答倒计时与权限切换等逻辑分支多。静态阅读未发现确定性错误,但缺少端到端回归,建议在无头浏览器中补充点击流验证(选项乱序shuffleOpts缓存一致性、材料大题答错停留计时、收题后重建状态)。
B3(兼容性提示)— 复制按钮依赖安全上下文¶
uc.jscopyText在非https/非isSecureContext时回退document.execCommand("copy")。局域网http://IP场景下 clipboard API 不可用属预期,回退路径已覆盖;但若未来强制 HTTPS,需确认execCommand被废弃后的兼容。
5. 优化建议汇总¶
| 编号 | 建议 | 收益 | 工作量 | 优先级 | 状态 |
|---|---|---|---|---|---|
| P0 | HTML 响应开启 gzip(服务层或反代层) | 公网首屏传输 -50%~70% | 低 | 高 | ✅ 已实施(Starlette GZipMiddleware) |
| P1 | 首页/场控内联脚本抽离为缓存静态文件 | 二次访问首页/场控传输→≈0 | 中 | 高 | ✅ 已实施(外联 static/index.js、static/admin.js) |
| P2 | 实时页轮询兜底放宽 + 确认 WS 透传 | 断线场景请求量下降 84% | 低 | 中 | ✅ 已实施(800ms→5s;WS 重连即刷新) |
| P2 | 组卷拉取改服务端搜索+分页,避免 limit=2000 |
组卷接口单次传输 -92.9% | 中 | 中 | ✅ 已实施(复用已有筛选 + 新增分页条) |
| B1 | ucMount 监听幂等化(命名函数 + remove) |
消除监听器泄漏 | 低 | 中 | ✅ 已实施(模块级一次性绑定) |
| B2 | 补充无头浏览器端到端点击流回归 | 拦截交互逻辑缺陷 | 中 | 中 | ✅ 已实施(_e2e_smoke.py,47/47 通过) |
6. 已实施的优化与复测结果(2026-09-15)¶
6.1 改动清单¶
- P0 HTML gzip:
main.py注册starlette.middleware.gzip.GZipMiddleware(minimum_size=1024, compresslevel=6)。 直接用 Starlette 内置中间件而非自写,原因是TemplateResponse的 body 在send阶段才渲染、自定义@app.middleware层取不到resp.body(实测body为None);内置中间件在 ASGI 流上缓冲压缩,且自动跳过已带content-encoding的静态资源、<1024B 小响应、不支持 gzip 的客户端,绝不重复压缩。 - P1 内联脚本外联:
templates/index.html与templates/admin.html的页面逻辑内联块抽离为static/index.js(54.6KB)、static/admin.js(63.5KB),模板改为<script src="{{ sv('/static/xxx.js') }}"></script>。 二者随CachedStaticFiles获得max-age=86400+ gzip;sv()版本号随文件变更自动击穿旧缓存。HTML 本身仍由html_no_store设no-store(改动即时生效)。 - B1 监听幂等化:
static/uc.js的ucMount不再每次document.addEventListener,改为模块加载时一次性绑定(闭包引用模块级HOST/closeMenu/closeModal),renderNav多次调用不再重复绑定,消除泄漏/重复执行。
6.2 复测数据(新代码,本地 8138 端口实测)¶
| 页面 | 状态 | 请求数 | HTML 原始→gzip | 总原始KB | 总gzip后KB | gzip节省 |
|---|---|---|---|---|---|---|
| 首页 / | 200 | 7 | 7.4→2.7 | 177.8 | 54.9 | 69.1% |
| 整卷测验 /exam | 200 | 7 | 3.8→1.7 | 138.2 | 44.3 | 67.9% |
| 我的记录 /me | 200 | 5 | 2.5→1.1 | 70.6 | 22.8 | 67.8% |
| 登录 /login | 200 | 2 | 6.1→2.7 | 42.7 | 12.4 | 71% |
| 个人资料 /profile | 200 | 2 | 3.1→1.7 | 39.7 | 11.3 | 71.4% |
6.3 前后对比(首页 / 为例)¶
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| HTML 传输 | 69.8KB(明文) | 2.7KB(gzip) | -96% |
| 首页总 gzip 传输 | 102.7KB | 54.9KB | -47% |
| 二次访问静态 | 重下 57KB 内联 | 命中 max-age=86400 跳过 |
≈0 额外传输 |
场控页
/admin.html模板同样已被 gzip(content-encoding: gzip);static/admin.js以max-age=86400缓存。管理端页面在登录态下整体首屏传输显著下降。
6.4 验证¶
- 三个 JS 文件
node --check通过;静态资源index.js/admin.js返回content-encoding: gzip+cache-control: public, max-age=86400(未二次压缩);HTML 与 JSON 均带content-encoding: gzip且vary: Accept-Encoding。 /admin未登录仍正确 302 跳转/login;/admin.html未登录返回登录页(含"登录"、无后台内容),无越权暴露。
7. 第三轮:P2 优化落地与 B2 端到端回归(2026-09-15)¶
7.1 改动清单¶
P2-a 实时页轮询兜底放宽 + WS 透传确认(static/index.js)
- WS 断线兜底轮询 800ms → 5000ms:断线场景每人请求量由 1.25 req/s 降至 0.2 req/s(-84%);WS 连通时仍按原 10s 心跳,实时性不受影响(实时推送走 WS)。
- ws.onopen 增加一次 refreshNow():WS(重)连成功后立即同步服务端状态,消除"重连后要等下一拍"的状态空窗。
P2-b 组卷候选改服务端分页(static/qbank.js、templates/qbank.html)
- 废弃 apiGet("/api/qbank/qs?parent=-1&limit=2000") 全量拉取,改为按 limit/offset 真分页(默认 50/页,可选 20/50/100 并 localStorage 记忆)。
- 复用组卷面板已有的筛选控件(关键词/题型/标签):筛选变更回到第 1 页,翻页保持筛选;分页条复用全站 createPager(新增 #pk-pager)。
- renderPick 的大题头索引由"当前页"改为跨页累积的 allQ,分页后小题仍能正确标注所属材料大题。
- 「随机抽 N 题」改用 GET /api/qbank/qs/ids(只返回 id,实测 1.7KB)抽样后按需取详情,替代原先拉 2000 行全量;保持"排除材料大题/编程题/小题"的原有语义。
- 兜底:createPager 不可用时仍回退一次性加载,功能不中断。
B2 无头浏览器端到端点击流回归(新增 _e2e_smoke.py)
- 技术选型:Playwright(Python 1.62) + 系统 Edge(channel="msedge"),无需下载 100MB+ Chromium。
- 自建临时 SQLite 库(复制 quiz_clean.db)+ 自动起服务 + 播种测试账号与 60 道题,跑完自动清理,不触碰开发用 MySQL 库。
- 覆盖:10 项源码回归断言 + 匿名页点击流 + 登录点击流 + 管理端 6 个页面 + 组卷分页回归 + WebSocket 透传,共 47 项断言。
7.2 实测数据:组卷候选接口传输量(944 题,gzip 后)¶
| 请求 | 传输量(gzip) | TTFB | 返回条数 |
|---|---|---|---|
旧:?parent=-1&limit=2000 |
131.2KB | 49.0ms | 944 |
新:?parent=-1&limit=50&offset=0 |
9.3KB | 4.7ms | 50 |
随机抽题:/qs/ids?parent=-1 |
1.7KB | 2.0ms | 944(id) |
单次组卷筛选传输量下降 92.9%,TTFB 下降约 90%;同时避免了 944~2000 张卡片的一次性
innerHTML渲染(DOM 节点数由 944 降至 50/页)。
7.3 端到端回归结果(python _e2e_smoke.py)¶
关键断言(节选):
- 源码回归:P2-a 间隔 5000、WS 重连刷新、组卷无 limit=2000、pkPager/#pk-pager、/qs/ids、gzip 中间件、内联脚本外联等 10 项。
- 页面点击流:/、/login、/exam、/admin(场控)、/admin/rooms、/admin/users、/admin/stats、/me、/profile 全部 HTTP 200、外联脚本执行成功、无 console 报错、无 4xx/5xx 资源。
- 登录点击流:真实填表(选学校 + 账号 + 密码)→ POST /api/auth/login 200 → 跳转离开 /login。
- 组卷分页:请求为 limit=50&offset=0 → 点「下一页」变 offset=50;分页条显示"共 944 条";全流程无 limit=2000。
- WebSocket:建房后连 /ws 握手成功并收到服务端推送(rank)。
唯一"非缺陷"现象:/admin 若不带房间上下文(?room=)直接打开,/api/current、/api/rank、/api/admin/questions 会返回 400("请先创建或加入房间")。这是设计要求——教师需先建房/选房(rooms.js 会把本页 ?room= 注入所有 /api 请求);按真实流程带 ?room= 进入则全部 200、无任何报错。
7.4 复跑方式¶
python _e2e_smoke.py # 自动建临时库+起服务+回归,跑完自动清理
python _e2e_smoke.py --keep # 保留临时库与服务日志(排查用)
python _e2e_smoke.py --base-url http://127.0.0.1:8000 # 复核已在运行的服务
8. 结论与下一步¶
结论:前端整体性能在局域网场景优秀(TTFB≈1ms、静态资源 gzip+长缓存、重型引擎懒加载)。三轮优化已全部落地:P0(HTML gzip)、P1(内联脚本外联缓存)、B1(监听幂等化)、P2-a(轮询兜底放宽 + WS 透传)、P2-b(组卷服务端分页)、B2(端到端回归)。 - 首页总首屏传输由 102.7KB 降至 54.9KB(-47%),HTML 由明文 69.8KB 降至 gzip 2.7KB(-96%),二次访问静态资源近乎零传输。 - 大题库组卷单次筛选传输由 131.2KB 降至 9.3KB(-92.9%),TTFB 由 49ms 降至 4.7ms。 - WS 断线兜底请求量下降 84%(1.25 → 0.2 req/s/人)。 - 端到端点击流回归 47/47 全通过,并沉淀为可复跑脚本。
下一步:
1. 用户重启服务以加载新代码(当前 8000 端口运行的仍是旧进程)。
2. 将 _e2e_smoke.py 纳入发版前例检:改动前端后跑一次,防止交互回归。
3. 如需渲染层指标(FCP/LCP/CLS/TTI),可在同一 Playwright 会话中加 PerformanceObserver 采集——依赖已就绪,无需再装浏览器。
复测脚本与原始/对比测量数据见工作区
_perf_measure.mjs(网络层采集,可重复运行:$env:BASE="http://127.0.0.1:8000"; node _perf_measure.mjs)。