为什么 Wiki.js 首屏要等两秒?一份完整的性能优化实战复盘
2026/9/3 22:18:09 网站建设 项目流程

为什么 Wiki.js 首屏要等两秒?一份完整的性能优化实战复盘

【免费下载链接】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.8 秒,编辑保存后还得盯着转圈。这份 Wiki.js 性能优化实录覆盖反向代理、服务端、构建、进程四处改动,最终把平均首屏压进 0.9 秒以内,编辑后的等待基本消失。

先查体:怎么证明不是机器的锅

问题刚出现时,第一反应是加内存、扩带宽,上线后体感毫无变化。别再猜了,直接上证据。三个检查,各给一个可操作的动作:

  1. 浏览器 Network 面板:勾掉"停用缓存"后刷新页面,按总耗时排序。结果很直白——最贵的条目不是图片,而是两个 JS/CSS 大文件,且每次访问都完整重下。这就是典型的 Wiki.js 首屏慢症状:资源太大,浏览器没有复用。
  2. 服务端请求日志:给渲染入口加计时。日志显示每个匿名浏览都完整跑了一遍管线——Markdown 渲染、目录树解析、HTML 后处理,读者不登录,这活儿基本白干。
  3. SQL 日志:开关在 server/core/config.js 里已经预留,config.ymlflags下加一行sqllog: true,服务端就会打印每条 SQL。跑一天,N+1 查询自己会跳出来;定位完记得关掉,它很吵。

三查下来结论明确:机器没问题,慢在浏览器没记住、服务端在返工、数据库在排队。

处方一:反向代理层——给静态资源一年缓存,只压文本

看到什么:Network 面板里同一批 JS/CSS 每次访问都重下。根因是前置 Nginx 没告诉浏览器"这些文件不用再问了"。

改哪一处:Nginx 配置做两件事——只给文本类资源开 Gzip,给/_assets/设置一年缓存:

gzip on; gzip_types text/css application/javascript application/json image/svg+xml; location /_assets/ { expires 1y; add_header Cache-Control "public, immutable"; }

为什么敢给一年?看 dev/webpack/webpack.prod.js:产物文件名自带时间戳,内容变了 URL 就变,新版本一定拿得到,老版本不会发错。

改完看到什么:这段 Nginx 静态资源缓存改动约十分钟,回访用户资源请求基本归零,文本类传输体积降约七成。

处方二:服务端层——给内存缓存贴"保质期",页面缓存只收匿名请求

看到什么:打开 server/core/cache.js,new NodeCache()没带任何参数——缓存键永不过期,内存只进不出,跑得越久堆积得越多。

改哪一处:Node.js 内存缓存配置的核心是补两个参数,stdTTL定缓存默认寿命,checkperiod定周期检查间隔:

init() { return new NodeCache({ stdTTL: 600, checkperiod: 120 }) }

等于给每条缓存贴了张"保质期"标签:十分钟到期自动失效,扫描线程定期清场。讲究一点再分层——字典类配置长 TTL,热点页面短 TTL,避免改完内容用户还看到旧版。

静态资源和内存缓存到位后,页面 HTML 每次仍要跑一遍渲染管线。对匿名读者占多数的知识库,我们把缓存再往前推一层到 Nginx,只缓存匿名 GET,命中直接吐 HTML:

proxy_cache_path /var/cache/wiki keys_zone=wiki:10m max_size=1g; location / { proxy_cache wiki; # 仅对匿名 GET 生效 proxy_cache_valid 200 5m; }

🔴红线提醒:登录态页面严禁缓存。不同账号同一地址可见内容不同,缓存串了就是安全事故。要么只对匿名请求启用,要么把鉴权信息带进缓存键并用Vary头区分响应。

改完看到什么:2.8 秒到 0.9 秒的跃升主要来自这一步,Nginx 日志里的缓存命中率可以直接核对。

处方三:构建层——splitChunks 与路由懒加载,主包瘦四成

看到什么:初始主包好几 MB,编辑器这种大多数人一辈子点不开的组件也在里面。

改哪一处:生产配置里 splitChunks 默认参数偏保守,改成让全部第三方依赖显式进独立 vendor chunk:

splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor' } } }

再加一步:Webpack 路由懒加载,编辑器只在用户点"编辑"时才下载:

// 点击"编辑"才拉取编辑器 chunk const Editor = () => import(/* webpackChunkName: "editor" */ './components/editor.vue')

改完看到什么:vendor chunk 几乎不变,长缓存命中率维持高位;应用更新只有小块 diff 需要重下。实测首屏 JS 体积降约四成。

处方四:进程层——连接池、独立 worker 与堆上限

看到什么:高峰期接口 p95 拉长、CPU 顶格。原因有二:数据库连接太少,请求排队;渲染是 CPU 密集任务,在和 Web 请求抢同一个事件循环。

改哪一处:三处。先说连接池——config.sample.yml里这块默认是注释状态,框架走保守默认值:

pool: min: 2 max: 8

把数据库连接想象成电梯,轿厢太少,所有人都在大厅干等。其次把 server/jobs/render-page.js 里的渲染任务挪进独立 worker 进程,Web 请求不再和重渲染抢事件循环。最后多实例时给 V8 堆设上限(约取机器内存的四分之一),防止 GC 抖动拖慢响应。

改完看到什么:高峰等待肉眼可见下降。注意:多实例务必打开配置里的ha标志——各实例内存缓存互相独立,需要发布流程统一失效才能保持一致。

避坑手册:这些"优化"我们后来全回滚了

⚠️ 做过的、放弃的,都写出来:

  • Gzip 连图片一起压:最初把图片也塞进gzip_types,CPU 飙升、体积几乎没降,后来只留文本类型。
  • 页面缓存 TTL 拉到一小时:用户反馈"明明改了,同事看到的还是旧的"。改回 5 分钟 + 发布时主动失效。
  • chunk 拆得太碎:一个功能拆出三四个 chunk,HTTP/1.1 下请求数爆炸反而更慢,后来合并并升级 HTTP/2。
  • 无脑横向扩容:把实例复制成四份,结果共享同一套数据库、各自维护缓存,症状不变。先把慢查询修掉,再谈扩容。

数据与清单

指标优化前优化后
平均首屏耗时2.8 秒0.9 秒
回访时的资源请求完整 JS/CSS 重下基本为零
初始主包体积(传输)约 1.2 MB约 0.7 MB
高峰期接口 p95约 1.5 秒约 0.5 秒
内存增长趋势持续上涨不回落随 TTL 波动,不漂移

📊 今天就能动手的三件事:

  1. 反向代理层给/_assets/加 Gzip 和一年缓存,纯 Nginx 配置,十分钟见效;
  2. 打开 server/core/cache.js 补上stdTTLcheckperiod,重启服务;
  3. 开启flags: sqllog跑一天揪出慢查询与 N+1,结束后记得关闭。

照着"从外到内"的顺序继续小步改,每一步都用数字说话,"能用"迟早会变成"好用"。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询