news 2026/8/23 13:43:51

Wiki.js 性能优化:首屏从 2.8 秒到 0.9 秒——一次完整的诊断、度量、修复、验收复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Wiki.js 性能优化:首屏从 2.8 秒到 0.9 秒——一次完整的诊断、度量、修复、验收复盘

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 页面加载慢的原因:先查证据,别急着修

排查前我们只列症状,不猜原因:

  1. 首屏 2 秒以上,资源在重复下载。打开浏览器 Network 面板看 Size 和 From Disk Cache 两列:JS/CSS 合计约 1.9MB,缓存列基本是空的,说明每次访问都在重传。
  2. 匿名请求每次都在走完整渲染。服务端日志里几乎每个页面请求都打了一条Rendering page ID记录——这是 server/jobs/render-page.js 渲染 job 的日志,说明 Markdown 渲染管线加 cheerio 目录解析对每个访问都完整跑了一遍。
  3. 高峰期接口排队。300 并发下 p95 从 300ms 爬到 800ms 以上;看 config.sample.yml 发现pool段整体被注释,连接池落在保守默认值上。
  4. 内存只涨不降。进程常驻 30 天从 300MB 涨到 1.4GB 且不再回落;对应 server/core/cache.js 里的new NodeCache()没有任何参数,缓存键没有过期机制。

一句话结论:机器没病,卡点在于该缓存的没缓存、该算一次的在重复算、该放行的连接在排队。

【度量】建立 Wiki.js 提速基线:统一口径,先拿"病历"

全文指标口径固定,避免自说自话:

  • 首屏 LCP:Performance 面板录 5 次冷启动取中位数;
  • 接口 p95:wrk 打 300 并发、持续 60 秒取 95 分位;
  • 首屏传输体积:Network 面板 transfer size 求和(压缩后口径)。
指标优化前测量方式
首屏 LCP2.8sPerformance 面板 5 次冷启动中位数
首屏传输体积约 1.9MBNetwork 面板 transfer size 求和
接口 p95(300 并发)约 850mswrk 300 并发 60s 压测
进程内存(运行 30 天)1.4GB 且持续上涨常驻监控取峰值

【处方】降低 Wiki.js 首屏延迟的三处修改:每张卡对应一个症状

和看病一样,病历拿齐才开始开方。以下三张处方卡,每张只解决一个症状。

症状一:每次访问都重复下载整套 JS 和 CSS

症状:首屏传输约 1.9MB;老用户再次访问仍产生大量 200 响应。

根因:dev/webpack/webpack.prod.js 里output.filenamejs/[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 这一刀;同时把MomentTimezoneDataPluginstartYear/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 提速前后数据,固化监控机制

指标口径与度量节一一对应:

指标优化前优化后测量方式(同前)
首屏 LCP2.8s0.9sPerformance 面板 5 次冷启动中位数
首屏传输体积约 1.9MB首访约 0.6MB / 复访约 0.2MBNetwork 面板 transfer size
接口 p95(300 并发)约 850ms约 320ms(预期)wrk 300 并发 60s
进程内存(30 天)1.4GB 持续上涨稳定 400MB 内常驻监控峰值

长效机制四条,防止回退:

  • 每周跑一次 300 并发 60 秒压测,p95 超过 500ms 触发告警并复查慢查询日志;
  • 每次前端发版后重测 LCP,超过基线 20% 就回滚构建参数改动;
  • 每月检查一次进程常驻与数据库慢查询日志,flags.sqllog只在排障时开一天;
  • 新增实例前确认ha与统一缓存失效流程已就绪,再谈扩容。

复盘到此收束:一次只改一处,每处都挂上数字,让数据划线。现在 10 分钟内能做完的三件事:

  1. 给反代/_assets/加 gzip 与一年长缓存;
  2. 给 server/core/cache.js 补上stdTTLcheckperiod,重启服务;
  3. 开启flags.sqllog跑一天,揪完慢查询再关掉。

【免费下载链接】wiki-Wiki.js | A modern and powerful wiki app built on Node.js项目地址: https://gitcode.com/GitHub_Trending/wiki78/wiki-

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 13:41:28

告别手打括号:Neovim 自动括号插件 Nvim-Autopairs 上手

告别手打括号:Neovim 自动括号插件 Nvim-Autopairs 上手 【免费下载链接】nvim-autopairs autopairs for neovim written in lua 项目地址: https://gitcode.com/gh_mirrors/nv/nvim-autopairs 写代码时,你打过多少次错位的括号,又回头…

作者头像 李华
网站建设 2026/8/23 13:36:42

用 BiliTools 把 1 小时视频压成 5 分钟笔记:AI 总结全流程

用 BiliTools 把 1 小时视频压成 5 分钟笔记:AI 总结全流程 【免费下载链接】BiliTools 本项目已停止维护。 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools 晚上十点,你第三次把那条 1 小时的技术教程拖回开头。这次不拖进度条…

作者头像 李华
网站建设 2026/8/23 13:35:56

德国技术面试中的算法挑战与优化策略

1. 德国技术面试中的算法挑战解析在德国科技企业的技术面试中,算法题向来是考察候选人编程基本功和逻辑思维能力的试金石。最近我参加了几场柏林和慕尼黑知名企业的面试,其中有两道颇具代表性的题目反复出现——经典的"两数之和"问题和一个有趣…

作者头像 李华
网站建设 2026/8/23 13:32:37

亚太杯数学建模竞赛:从高效组队到系统性备赛的实战指南

1. 项目概述:从“组队喊人”到“系统性备赛”的认知跃迁 “2023年亚太杯数学建模竞赛赶紧上车差2个人保2冲1”——这个在各大高校论坛、社群和朋友圈里高频出现的标题,背后远不止是一则简单的组队招募启事。它精准地戳中了数模竞赛参与者的核心痛点&…

作者头像 李华