Wiki.js 性能优化:首屏从 2.8 秒到 0.9 秒——一次完整的诊断、度量、修复、验收复盘
【免费下载链接】wiki-Wiki.js | A modern and powerful wiki app built on Node.js项目地址: https://gitcode.com/GitHub_Trending/wiki78/wiki-
我们用 Wiki.js(Node.js 知识库系统)跑一套内部知识库,升级后匿名首屏要等 2 秒多,编辑长文档后转圈 3~4 秒。本文用真实数据复盘这次 Wiki.js 性能优化全过程:诊断、度量、修复、验收与固化。
【诊断】定位 Wiki.js 页面加载慢的原因:先查证据,别急着修
排查前我们只列症状,不猜原因:
- 首屏 2 秒以上,资源在重复下载。打开浏览器 Network 面板看 Size 和 From Disk Cache 两列:JS/CSS 合计约 1.9MB,缓存列基本是空的,说明每次访问都在重传。
- 匿名请求每次都在走完整渲染。服务端日志里几乎每个页面请求都打了一条
Rendering page ID记录——这是 server/jobs/render-page.js 渲染 job 的日志,说明 Markdown 渲染管线加 cheerio 目录解析对每个访问都完整跑了一遍。 - 高峰期接口排队。300 并发下 p95 从 300ms 爬到 800ms 以上;看 config.sample.yml 发现
pool段整体被注释,连接池落在保守默认值上。 - 内存只涨不降。进程常驻 30 天从 300MB 涨到 1.4GB 且不再回落;对应 server/core/cache.js 里的
new NodeCache()没有任何参数,缓存键没有过期机制。
一句话结论:机器没病,卡点在于该缓存的没缓存、该算一次的在重复算、该放行的连接在排队。
【度量】建立 Wiki.js 提速基线:统一口径,先拿"病历"
全文指标口径固定,避免自说自话:
- 首屏 LCP:Performance 面板录 5 次冷启动取中位数;
- 接口 p95:wrk 打 300 并发、持续 60 秒取 95 分位;
- 首屏传输体积:Network 面板 transfer size 求和(压缩后口径)。
| 指标 | 优化前 | 测量方式 |
|---|---|---|
| 首屏 LCP | 2.8s | Performance 面板 5 次冷启动中位数 |
| 首屏传输体积 | 约 1.9MB | Network 面板 transfer size 求和 |
| 接口 p95(300 并发) | 约 850ms | wrk 300 并发 60s 压测 |
| 进程内存(运行 30 天) | 1.4GB 且持续上涨 | 常驻监控取峰值 |
【处方】降低 Wiki.js 首屏延迟的三处修改:每张卡对应一个症状
和看病一样,病历拿齐才开始开方。以下三张处方卡,每张只解决一个症状。
症状一:每次访问都重复下载整套 JS 和 CSS
症状:首屏传输约 1.9MB;老用户再次访问仍产生大量 200 响应。
根因:dev/webpack/webpack.prod.js 里output.filename是js/[name].js?${now}——URL 自带构建时间戳查询串,同一构建内内容不可变,查询串等于版本号;但反代层没给/_assets/设长缓存头,浏览器每次访问都当新资源处理。
改法:在反代层加压缩与长缓存(8 行以内):
gzip on; gzip_comp_level 5; gzip_types text/css application/javascript application/json; location /_assets/ { expires 1y; add_header Cache-Control "public, immutable"; }为什么这么改:文件名变了才是新版本,不变就是一年长缓存,浏览器直接复用本地副本;gzip 只列文本类型,不碰图片二进制。
效果:复访传输从 1.9MB 降到约 0.2MB(本地命中),首访传输压缩后降约七成,LCP 减少约 0.6 秒。
症状二:并发升高时接口排队,内存只涨不降
症状:高峰期接口 p95 破 800ms;进程常驻 30 天涨到 1.4GB。
根因:server/core/cache.js 的init()只有new NodeCache()无参构造,缓存键无过期,历史页面数据只进不出;同时连接池未显式配置。为确认数据库侧开销,我们开启了flags.sqllog(server/app/data.yml 中默认 false,由 server/core/config.js 的applyFlags()映射到 Knex debug 打印全部 SQL),一天内揪出 3 处 N+1 查询和 1 条缺索引语句,定位完立即关闭。
改法:缓存加过期与定期清理:
return new NodeCache({ stdTTL: 600, checkperiod: 120 })连接池按样例自带建议值放开:
pool: min: 2 max: 10为什么这么改:stdTTL让热点页面 10 分钟到期自动失效,checkperiod定期清掉过期键,内存不再无限累积;pool 是 config.sample.yml 里本就注释好的官方建议值,只解除注释并重启。
效果:运行 30 天后内存稳定在 400MB 以内;同 300 并发下 p95 预期从 850ms 降到约 320ms。
症状三:主包体积大,更新后用户重传一大块
症状:主 JS 未压缩超 1.2MB,任何小改动都让用户重下大块 vendor。
根因:dev/webpack/webpack.prod.js 中optimization.splitChunks仅有{ name: 'vendor', minChunks: 2 },没开chunks: 'all',第三方库和应用代码混在同一个主包;MomentTimezoneDataPlugin还打进了 2017 年起加未来 5 年的全量时区表。
改法:显式把第三方库固定进独立、稳定的 vendor chunk:
splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor', priority: 10 } } }为什么这么改:客户端已对编辑器、管理后台等重路由做了动态 import 懒加载(见 client/client-app.js),主包只差 vendor 这一刀;同时把MomentTimezoneDataPlugin的startYear/endYear收窄到用户实际所在区间,预期再省约 200KB 时区表。另外构建侧cache-loader缓存目录.webpack-cache/已配置好,部署脚本别再清它,增量构建预计快 2~3 倍。
效果(预期):主包缩小约四成,版本更新后用户只需下载应用代码的小块 diff。
【避坑】回滚过的 Wiki.js 性能优化反模式
用 Gzip 连图片一起压:二进制图片压缩率几乎为零,CPU 白烧,高峰期更卡。正确做法:gzip_types只列文本类 MIME,保持上面 8 行配置不变。
把缓存 TTL 一味拉长:页面更新后用户长时间看到旧版,投诉比"慢"更伤。正确做法:短 TTL(分钟级)加发布时主动失效,用新鲜度换稳定。
在反代层缓存登录态页面:⚠️ 这是安全红线。同一 URL 对不同权限组内容不同,缓存命中即越权串数据。正确做法:只缓存匿名 GET,缓存键中携带鉴权信息,登录态响应保持 no-store。
多实例共享数据库却不处理缓存一致性:config.sample.yml 里ha标志存在就是为此——多个实例同库时,各实例内存缓存各管各的,A 实例改了内容 B 实例看不到。正确做法:开ha并配合统一失效流程再扩容。
把 chunk 拆得过于细碎:HTTP/1.1 下请求数爆炸,反而更慢。正确做法:先上 HTTP/2 再细拆,或维持"一个稳定 vendor + 懒加载路由"的粗粒度。
【验收与固化】对比 Wiki.js 提速前后数据,固化监控机制
指标口径与度量节一一对应:
| 指标 | 优化前 | 优化后 | 测量方式(同前) |
|---|---|---|---|
| 首屏 LCP | 2.8s | 0.9s | Performance 面板 5 次冷启动中位数 |
| 首屏传输体积 | 约 1.9MB | 首访约 0.6MB / 复访约 0.2MB | Network 面板 transfer size |
| 接口 p95(300 并发) | 约 850ms | 约 320ms(预期) | wrk 300 并发 60s |
| 进程内存(30 天) | 1.4GB 持续上涨 | 稳定 400MB 内 | 常驻监控峰值 |
长效机制四条,防止回退:
- 每周跑一次 300 并发 60 秒压测,p95 超过 500ms 触发告警并复查慢查询日志;
- 每次前端发版后重测 LCP,超过基线 20% 就回滚构建参数改动;
- 每月检查一次进程常驻与数据库慢查询日志,
flags.sqllog只在排障时开一天; - 新增实例前确认
ha与统一缓存失效流程已就绪,再谈扩容。
复盘到此收束:一次只改一处,每处都挂上数字,让数据划线。现在 10 分钟内能做完的三件事:
- 给反代
/_assets/加 gzip 与一年长缓存; - 给 server/core/cache.js 补上
stdTTL与checkperiod,重启服务; - 开启
flags.sqllog跑一天,揪完慢查询再关掉。
【免费下载链接】wiki-Wiki.js | A modern and powerful wiki app built on Node.js项目地址: https://gitcode.com/GitHub_Trending/wiki78/wiki-
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考