为什么 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 秒以内,编辑后的等待基本消失。
先查体:怎么证明不是机器的锅
问题刚出现时,第一反应是加内存、扩带宽,上线后体感毫无变化。别再猜了,直接上证据。三个检查,各给一个可操作的动作:
- 浏览器 Network 面板:勾掉"停用缓存"后刷新页面,按总耗时排序。结果很直白——最贵的条目不是图片,而是两个 JS/CSS 大文件,且每次访问都完整重下。这就是典型的 Wiki.js 首屏慢症状:资源太大,浏览器没有复用。
- 服务端请求日志:给渲染入口加计时。日志显示每个匿名浏览都完整跑了一遍管线——Markdown 渲染、目录树解析、HTML 后处理,读者不登录,这活儿基本白干。
- SQL 日志:开关在 server/core/config.js 里已经预留,
config.yml的flags下加一行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 波动,不漂移 |
📊 今天就能动手的三件事:
- 反向代理层给
/_assets/加 Gzip 和一年缓存,纯 Nginx 配置,十分钟见效; - 打开 server/core/cache.js 补上
stdTTL与checkperiod,重启服务; - 开启
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),仅供参考